diff --git a/.gitignore b/.gitignore index 8b9cc54a0..5f5db36d7 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,4 @@ $RECYCLE.BIN/ # Documentation docs/_site/ docs/.jekyll-metadata +docs/Gemfile.lock diff --git a/.travis.yml b/.travis.yml index 33e133203..01ad30886 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,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 tableauserverclient test + - pycodestyle tableauserverclient test samples diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fece541..a9b13a8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## 0.5.1 (21 Sept 2017 +## 0.6 (17 Jan 2018) + +* Added support for add a datasource/workbook refresh to a schedule (#244) +* Added support for updating datasource connections (#253) +* Added Refresh Now for datasource and workbooks (#250) +* Fixed Typos in the docs (#251) + +## 0.5.1 (21 Sept 2017) * Fix a critical issue caused by #224 that was the result of lack of test coverage (#226) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9ecc980ee..4143570cf 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,6 +14,7 @@ The following people have contributed to this project to make it possible, and w * [Russ Goldin](https://github.com/tagyoureit) * [William Lang](https://github.com/williamlang) * [Jim Morris](https://github.com/jimbodriven) +* [BingoDinkus](https://github.com/BingoDinkus) ## Core Team diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index e555d12be..000000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,129 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (4.2.6) - i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.4.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.10.0) - colorator (0.1) - ethon (0.9.0) - ffi (>= 1.3.0) - execjs (2.7.0) - faraday (0.9.2) - multipart-post (>= 1.2, < 3) - ffi (1.9.10) - ffi (1.9.10-x86-mingw32) - gemoji (2.1.0) - github-pages (80) - github-pages-health-check (= 1.1.0) - jekyll (= 3.1.6) - jekyll-coffeescript (= 1.0.1) - jekyll-feed (= 0.5.1) - jekyll-gist (= 1.4.0) - jekyll-github-metadata (= 1.11.1) - jekyll-mentions (= 1.1.2) - jekyll-paginate (= 1.1.0) - jekyll-redirect-from (= 0.10.0) - jekyll-sass-converter (= 1.3.0) - jekyll-seo-tag (= 1.4.0) - jekyll-sitemap (= 0.10.0) - jemoji (= 0.6.2) - kramdown (= 1.10.0) - liquid (= 3.0.6) - listen (= 3.0.6) - mercenary (~> 0.3) - rouge (= 1.10.1) - terminal-table (~> 1.4) - github-pages-health-check (1.1.0) - addressable (~> 2.3) - net-dns (~> 0.8) - octokit (~> 4.0) - public_suffix (~> 1.4) - typhoeus (~> 0.7) - html-pipeline (2.4.1) - activesupport (>= 2, < 5) - nokogiri (>= 1.4) - i18n (0.7.0) - jekyll (3.1.6) - colorator (~> 0.1) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 1.1) - kramdown (~> 1.3) - liquid (~> 3.0) - mercenary (~> 0.3.3) - rouge (~> 1.7) - safe_yaml (~> 1.0) - jekyll-coffeescript (1.0.1) - coffee-script (~> 2.2) - jekyll-feed (0.5.1) - jekyll-gist (1.4.0) - octokit (~> 4.2) - jekyll-github-metadata (1.11.1) - octokit (~> 4.0) - jekyll-mentions (1.1.2) - html-pipeline (~> 2.3) - jekyll (~> 3.0) - jekyll-paginate (1.1.0) - jekyll-redirect-from (0.10.0) - jekyll (>= 2.0) - jekyll-sass-converter (1.3.0) - sass (~> 3.2) - jekyll-seo-tag (1.4.0) - jekyll (~> 3.0) - jekyll-sitemap (0.10.0) - jekyll-watch (1.4.0) - listen (~> 3.0, < 3.1) - jemoji (0.6.2) - gemoji (~> 2.0) - html-pipeline (~> 2.2) - jekyll (>= 3.0) - json (1.8.3) - kramdown (1.10.0) - liquid (3.0.6) - listen (3.0.6) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9.7) - mercenary (0.3.6) - mini_portile2 (2.0.0) - minitest (5.9.0) - multipart-post (2.0.0) - net-dns (0.8.0) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) - nokogiri (1.6.7.2-x86-mingw32) - mini_portile2 (~> 2.0.0.rc2) - octokit (4.3.0) - sawyer (~> 0.7.0, >= 0.5.3) - public_suffix (1.5.3) - rb-fsevent (0.9.7) - rb-inotify (0.9.7) - ffi (>= 0.5.0) - rouge (1.10.1) - safe_yaml (1.0.4) - sass (3.4.22) - sawyer (0.7.0) - addressable (>= 2.3.5, < 2.5) - faraday (~> 0.8, < 0.10) - terminal-table (1.5.2) - thread_safe (0.3.5) - typhoeus (0.8.0) - ethon (>= 0.8.0) - tzinfo (1.2.2) - thread_safe (~> 0.1) - -PLATFORMS - ruby - x86-mingw32 - -DEPENDENCIES - github-pages - -BUNDLED WITH - 1.12.5 diff --git a/docs/docs/api-ref.md b/docs/docs/api-ref.md index 5b07b9884..7b22c3517 100644 --- a/docs/docs/api-ref.md +++ b/docs/docs/api-ref.md @@ -2575,7 +2575,7 @@ server = TSC.Server('http://servername') with server.auth.sign_in(tableau_auth): all_workbook_items, pagination_item = server.workbooks.get() - print([workbook.name for workbook in all_workbooks]) + print([workbook.name for workbook in all_workbook_items]) diff --git a/docs/docs/versions.md b/docs/docs/versions.md index 48531b187..eac3cdca2 100644 --- a/docs/docs/versions.md +++ b/docs/docs/versions.md @@ -32,7 +32,11 @@ import tableauserverclient as TSC server = TSC.Server('http://SERVER_URL') -server.version = '2.4' + +server.version = '2.6' + + + ``` ## Supported versions @@ -43,3 +47,5 @@ The current version of TSC only supports the following REST API and Tableau Serv |---|---| |2.3|10.0| |2.4|10.1| +|2.5|10.2| +|2.6|10.3| diff --git a/samples/create_project.py b/samples/create_project.py index c68b992a9..744b056d4 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -24,6 +24,7 @@ def create_project(server, project_item): print('We have already created this project: %s' % project_item.name) sys.exit(1) + def main(): parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') parser.add_argument('--server', '-s', required=True, help='server address') @@ -64,5 +65,6 @@ def main(): grand_child_project = TSC.ProjectItem(name='Grand Child Project', parent_id=child_project.id) grand_child_project = create_project(server, grand_child_project) + if __name__ == '__main__': main() diff --git a/samples/download_view_image.py b/samples/download_view_image.py index 6038bd337..2da232061 100644 --- a/samples/download_view_image.py +++ b/samples/download_view_image.py @@ -64,5 +64,6 @@ def main(): print("View image saved to {0}".format(args.filepath)) + if __name__ == '__main__': main() diff --git a/samples/explore_datasource.py b/samples/explore_datasource.py index b5fe68390..e740d60f1 100644 --- a/samples/explore_datasource.py +++ b/samples/explore_datasource.py @@ -79,5 +79,6 @@ def main(): sample_datasource.tags = original_tag_set server.datasources.update(sample_datasource) + if __name__ == '__main__': main() diff --git a/samples/filter_sort_groups.py b/samples/filter_sort_groups.py index 6ed6fc773..3b585327d 100644 --- a/samples/filter_sort_groups.py +++ b/samples/filter_sort_groups.py @@ -87,5 +87,6 @@ def main(): for group in matching_groups: print(group.name) + if __name__ == '__main__': main() diff --git a/samples/list.py b/samples/list.py new file mode 100644 index 000000000..ec2ff9a6b --- /dev/null +++ b/samples/list.py @@ -0,0 +1,51 @@ +#### +# This script demonstrates how to list all of the workbooks or datasources +# +# To run the script, you must have installed Python 2.7.X or 3.3 and later. +#### + +import argparse +import getpass +import logging + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') + parser.add_argument('--server', '-s', required=True, help='server address') + parser.add_argument('--username', '-u', required=True, help='username to sign into server') + parser.add_argument('--site', '-S', default=None) + parser.add_argument('-p', default=None) + + parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', + help='desired logging level (set to error by default)') + + parser.add_argument('resource_type', choices=['workbook', 'datasource']) + + args = parser.parse_args() + + if args.p is None: + password = getpass.getpass("Password: ") + else: + password = args.p + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + # SIGN IN + tableau_auth = TSC.TableauAuth(args.username, password, args.site) + server = TSC.Server(args.server, use_server_version=True) + with server.auth.sign_in(tableau_auth): + endpoint = { + 'workbook': server.workbooks, + 'datasource': server.datasources + }.get(args.resource_type) + + for resource in TSC.Pager(endpoint.get): + print(resource.id, resource.name) + + +if __name__ == '__main__': + main() diff --git a/samples/refresh.py b/samples/refresh.py new file mode 100644 index 000000000..dd39bc6f6 --- /dev/null +++ b/samples/refresh.py @@ -0,0 +1,54 @@ +#### +# This script demonstrates how to use trigger a refresh on a datasource or workbook +# +# To run the script, you must have installed Python 2.7.X or 3.3 and later. +#### + +import argparse +import getpass +import logging + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server') + parser.add_argument('--server', '-s', required=True, help='server address') + parser.add_argument('--username', '-u', required=True, help='username to sign into server') + parser.add_argument('--site', '-S', default=None) + parser.add_argument('-p', default=None) + + parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', + help='desired logging level (set to error by default)') + + parser.add_argument('resource_type', choices=['workbook', 'datasource']) + parser.add_argument('resource_id') + + args = parser.parse_args() + + if args.p is None: + password = getpass.getpass("Password: ") + else: + password = args.p + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + # SIGN IN + tableau_auth = TSC.TableauAuth(args.username, password, args.site) + server = TSC.Server(args.server, use_server_version=True) + with server.auth.sign_in(tableau_auth): + endpoint = { + 'workbook': server.workbooks, + 'datasource': server.datasources + }.get(args.resource_type) + + refresh_func = endpoint.refresh + resource = endpoint.get_by_id(args.resource_id) + + print(refresh_func(resource)) + + +if __name__ == '__main__': + main() diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py new file mode 100644 index 000000000..edb94f47e --- /dev/null +++ b/samples/set_refresh_schedule.py @@ -0,0 +1,85 @@ +import argparse +import getpass +import logging + +import tableauserverclient as TSC + + +def usage(args): + parser = argparse.ArgumentParser(description='Explore workbook functions supported by the Server API.') + parser.add_argument('--server', '-s', required=True, help='server address') + 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)') + parser.add_argument('--password', '-p', default=None) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--workbook', '-w') + group.add_argument('--datasource', '-d') + parser.add_argument('schedule') + + return parser.parse_args(args) + + +def make_filter(**kwargs): + options = TSC.RequestOptions() + for item, value in kwargs.items(): + name = getattr(TSC.RequestOptions.Field, item) + options.filter.add(TSC.Filter(name, TSC.RequestOptions.Operator.Equals, value)) + return options + + +def get_workbook_by_name(server, name): + request_filter = make_filter(Name=name) + workbooks, _ = server.workbooks.get(request_filter) + assert len(workbooks) == 1 + return workbooks.pop() + + +def get_datasource_by_name(server, name): + request_filter = make_filter(Name=name) + datasources, _ = server.datasources.get(request_filter) + assert len(datasources) == 1 + return datasources.pop() + + +def get_schedule_by_name(server, name): + schedules = [x for x in TSC.Pager(server.schedules) if x.name == name] + assert len(schedules) == 1 + return schedules.pop() + + +def assign_to_schedule(server, workbook_or_datasource, schedule): + server.schedules.add_to_schedule(schedule.id, workbook_or_datasource) + + +def run(args): + password = args.password + if password is None: + 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, use_server_version=True) + + with server.auth.sign_in(tableau_auth): + if args.workbook: + item = get_workbook_by_name(server, args.workbook) + else: + item = get_datasource_by_name(server, args.datasource) + schedule = get_schedule_by_name(server, args.schedule) + + assign_to_schedule(server, item, schedule) + + +def main(): + import sys + args = usage(sys.argv[1:]) + run(args) + + +if __name__ == "__main__": + main() diff --git a/samples/update_connection.py b/samples/update_connection.py new file mode 100644 index 000000000..36001b379 --- /dev/null +++ b/samples/update_connection.py @@ -0,0 +1,63 @@ +#### +# This script demonstrates how to update a connections credentials on a server to embed the credentials +# +# To run the script, you must have installed Python 2.7.X or 3.3 and later. +#### + +import argparse +import getpass +import logging + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description='Update a connection on a datasource or workbook to embed credentials') + parser.add_argument('--server', '-s', required=True, help='server address') + parser.add_argument('--username', '-u', required=True, help='username to sign into server') + parser.add_argument('--site', '-S', default=None) + parser.add_argument('-p', default=None) + + parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', + help='desired logging level (set to error by default)') + + parser.add_argument('resource_type', choices=['workbook', 'datasource']) + parser.add_argument('resource_id') + parser.add_argument('connection_id') + parser.add_argument('datasource_username') + parser.add_argument('datasource_password') + + args = parser.parse_args() + + if args.p is None: + password = getpass.getpass("Password: ") + else: + password = args.p + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + # SIGN IN + tableau_auth = TSC.TableauAuth(args.username, password, args.site) + server = TSC.Server(args.server, use_server_version=True) + with server.auth.sign_in(tableau_auth): + endpoint = { + 'workbook': server.workbooks, + 'datasource': server.datasources + }.get(args.resource_type) + + update_function = endpoint.update_connection + resource = endpoint.get_by_id(args.resource_id) + endpoint.populate_connections(resource) + connections = list(filter(lambda x: x.id == args.connection_id, resource.connections)) + assert(len(connections) == 1) + connection = connections[0] + connection.username = args.datasource_username + connection.password = args.datasource_password + connection.embed_password = True + print(update_function(resource, connection).content) + + +if __name__ == '__main__': + main() diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 440d99aaf..c5840d7b6 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,11 +1,11 @@ from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\ - GroupItem, PaginationItem, ProjectItem, ScheduleItem, \ + GroupItem, JobItem, PaginationItem, ProjectItem, ScheduleItem, \ SiteItem, TableauAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \ - HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem -from .server import RequestOptions, ImageRequestOptions, Filter, Sort, Server, ServerResponseError,\ - MissingRequiredFieldError, NotSignedInError, Pager - + HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \ + SubscriptionItem +from .server import RequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \ + Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager from ._version import get_versions __version__ = get_versions()['version'] __VERSION__ = __version__ diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index cb26f4eaa..1ff6be869 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -4,6 +4,7 @@ from .exceptions import UnpopulatedPropertyError from .group_item import GroupItem from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval +from .job_item import JobItem from .pagination_item import PaginationItem from .project_item import ProjectItem from .schedule_item import ScheduleItem @@ -14,3 +15,4 @@ from .user_item import UserItem from .view_item import ViewItem from .workbook_item import WorkbookItem +from .subscription_item import SubscriptionItem diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py index 3f2491170..a52d32e9e 100644 --- a/tableauserverclient/models/connection_item.py +++ b/tableauserverclient/models/connection_item.py @@ -29,6 +29,10 @@ def id(self): def connection_type(self): return self._connection_type + def __repr__(self): + return ""\ + .format(**self.__dict__) + @classmethod def from_response(cls, resp, ns): all_connection_items = list() diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 3ecbe9e4f..b00e6cbea 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -28,7 +28,7 @@ def connections(self): if self._connections is None: error = 'Datasource item must be populated with connections first.' raise UnpopulatedPropertyError(error) - return self._connections + return self._connections() @property def content_url(self): diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py new file mode 100644 index 000000000..cc53765ed --- /dev/null +++ b/tableauserverclient/models/job_item.py @@ -0,0 +1,60 @@ +import xml.etree.ElementTree as ET +from .target import Target + + +class JobItem(object): + def __init__(self, id_, job_type, created_at, started_at=None, completed_at=None, finish_code=0): + self._id = id_ + self._type = job_type + self._created_at = created_at + self._started_at = started_at + self._completed_at = completed_at + self._finish_code = finish_code + + @property + def id(self): + return self._id + + @property + def type(self): + return self._type + + @property + def created_at(self): + return self._created_at + + @property + def started_at(self): + return self._started_at + + @property + def completed_at(self): + return self._completed_at + + @property + def finish_code(self): + return self._finish_code + + def __repr__(self): + return "".format(**self.__dict__) + + @classmethod + def from_response(cls, xml, ns): + parsed_response = ET.fromstring(xml) + all_tasks_xml = parsed_response.findall( + './/t:job', namespaces=ns) + + all_tasks = [JobItem._parse_element(x, ns) for x in all_tasks_xml] + + return all_tasks + + @classmethod + def _parse_element(cls, element, ns): + id_ = element.get('id', None) + type_ = element.get('type', None) + created_at = element.get('createdAt', None) + started_at = element.get('startedAt', None) + completed_at = element.get('completedAt', None) + finish_code = element.get('finishCode', -1) + return cls(id_, type_, created_at, started_at, completed_at, finish_code) diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index 5531ef090..3e97ccc15 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -103,7 +103,6 @@ def updated_at(self): def _parse_common_tags(self, schedule_xml, ns): if not isinstance(schedule_xml, ET.Element): - print(ns) schedule_xml = ET.fromstring(schedule_xml).find('.//t:schedule', namespaces=ns) if schedule_xml is not None: (_, name, _, _, updated_at, _, next_run_at, end_schedule_at, execution_order, diff --git a/tableauserverclient/models/subscription_item.py b/tableauserverclient/models/subscription_item.py new file mode 100644 index 000000000..5a99fefc2 --- /dev/null +++ b/tableauserverclient/models/subscription_item.py @@ -0,0 +1,57 @@ +import xml.etree.ElementTree as ET +from .exceptions import UnpopulatedPropertyError +from .target import Target + + +class SubscriptionItem(object): + + def __init__(self, subject, schedule_id, user_id, target): + self.id = None + self.subject = subject + self.schedule_id = schedule_id + self.user_id = user_id + self.target = target + + def __repr__(self): + if self.id is not None: + return "= 300: + return AddResponse(result=False, + error="Status {}: {}".format(response.status_code, response.reason)) + logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) + return OK + + items = [] + + if workbook is not None: + items.append((workbook, "workbook", RequestFactory.Schedule.add_workbook_req)) + if datasource is not None: + items.append((datasource, "datasource", RequestFactory.Schedule.add_datasource_req)) + + results = (add_to(*x) for x in items) + # list() is needed for python 3.x compatibility + return list(filter(lambda x: not x.result, results)) diff --git a/tableauserverclient/server/endpoint/subscriptions_endpoint.py b/tableauserverclient/server/endpoint/subscriptions_endpoint.py new file mode 100644 index 000000000..70422e208 --- /dev/null +++ b/tableauserverclient/server/endpoint/subscriptions_endpoint.py @@ -0,0 +1,53 @@ +from .endpoint import Endpoint, api +from .exceptions import MissingRequiredFieldError +from .. import RequestFactory, SubscriptionItem, PaginationItem +import logging + +logger = logging.getLogger('tableau.endpoint.subscriptions') + + +class Subscriptions(Endpoint): + @property + def baseurl(self): + return "{0}/sites/{1}/subscriptions".format(self.parent_srv.baseurl, + self.parent_srv.site_id) + + @api(version='2.3') + def get(self, req_options=None): + logger.info('Querying all subscriptions for the site') + url = self.baseurl + server_response = self.get_request(url, req_options) + + pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) + all_subscriptions = SubscriptionItem.from_response(server_response.content, self.parent_srv.namespace) + return all_subscriptions, pagination_item + + @api(version='2.3') + def get_by_id(self, subscription_id): + if not subscription_id: + error = "No Subscription ID provided" + raise ValueError(error) + logger.info("Querying a single subscription by id ({})".format(subscription_id)) + url = "{}/{}".format(self.baseurl, subscription_id) + server_response = self.get_request(url) + return SubscriptionItem.from_response(server_response.content, self.parent_srv.namespace)[0] + + @api(version='2.3') + def create(self, subscription_item): + if not subscription_item: + error = "No Susbcription provided" + raise ValueError(error) + logger.info("Creating a subscription ({})".format(subscription_item)) + url = self.baseurl + create_req = RequestFactory.Subscription.create_req(subscription_item) + server_response = self.post_request(url, create_req) + return SubscriptionItem.from_response(server_response.content, self.parent_srv.namespace)[0] + + @api(version='2.3') + def delete(self, subscription_id): + if not subscription_id: + error = "Subscription ID undefined." + raise ValueError(error) + url = "{0}/{1}".format(self.baseurl, subscription_id) + self.delete_request(url) + logger.info('Deleted subscription (ID: {0})'.format(subscription_id)) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index 21a3d4742..64b296543 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -1,6 +1,7 @@ from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, UserItem, WorkbookItem, PaginationItem +from ..pager import Pager import logging import copy @@ -73,12 +74,19 @@ def populate_workbooks(self, user_item, req_options=None): if not user_item.id: error = "User item missing ID." raise MissingRequiredFieldError(error) + + def wb_pager(): + return Pager(lambda options: self._get_wbs_for_user(user_item, options), req_options) + + user_item._set_workbooks(wb_pager) + + def _get_wbs_for_user(self, user_item, req_options=None): url = "{0}/{1}/workbooks".format(self.baseurl, user_item.id) server_response = self.get_request(url, req_options) logger.info('Populated workbooks for user (ID: {0})'.format(user_item.id)) - user_item._set_workbooks(WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)) + workbook_item = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) - return pagination_item + return workbook_item, pagination_item def populate_favorites(self, user_item): raise NotImplementedError('REST API currently does not support the ability to query favorites') diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index 4d9909a6c..0335ce781 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -4,6 +4,7 @@ from .. import RequestFactory, ViewItem, PaginationItem from ...models.tag_item import TagItem import logging +from contextlib import closing logger = logging.getLogger('tableau.endpoint.views') @@ -23,9 +24,12 @@ def baseurl(self): return "{0}/views".format(self.siteurl) @api(version="2.2") - def get(self, req_options=None): + def get(self, req_options=None, usage=False): logger.info('Querying all views on site') - server_response = self.get_request(self.baseurl, req_options) + url = self.baseurl + if usage: + url += "?includeUsageStatistics=true" + server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) all_view_items = ViewItem.from_response(server_response.content, self.parent_srv.namespace) return all_view_items, pagination_item @@ -35,21 +39,75 @@ def populate_preview_image(self, view_item): if not view_item.id or not view_item.workbook_id: error = "View item missing ID or workbook ID." raise MissingRequiredFieldError(error) + + def image_fetcher(): + return self._get_preview_for_view(view_item) + + view_item._set_preview_image(image_fetcher) + logger.info('Populated preview image for view (ID: {0})'.format(view_item.id)) + + def _get_preview_for_view(self, view_item): url = "{0}/workbooks/{1}/views/{2}/previewImage".format(self.siteurl, view_item.workbook_id, view_item.id) server_response = self.get_request(url) - view_item._preview_image = server_response.content - logger.info('Populated preview image for view (ID: {0})'.format(view_item.id)) + image = server_response.content + return image + @api(version="2.5") def populate_image(self, view_item, req_options=None): if not view_item.id: error = "View item missing ID." raise MissingRequiredFieldError(error) + + def image_fetcher(): + return self._get_view_image(view_item, req_options) + + view_item._set_image(image_fetcher) + logger.info("Populated image for view (ID: {0})".format(view_item.id)) + + def _get_view_image(self, view_item, req_options): url = "{0}/{1}/image".format(self.baseurl, view_item.id) server_response = self.get_request(url, req_options) - view_item._image = server_response.content - logger.info("Populated image for view (ID: {0})".format(view_item.id)) + image = server_response.content + return image + + @api(version="2.7") + def populate_pdf(self, view_item, req_options=None): + if not view_item.id: + error = "View item missing ID." + raise MissingRequiredFieldError(error) + + def pdf_fetcher(): + return self._get_view_pdf(view_item, req_options) + + view_item._set_pdf(pdf_fetcher) + logger.info("Populated pdf for view (ID: {0})".format(view_item.id)) + + def _get_view_pdf(self, view_item, req_options): + url = "{0}/{1}/pdf".format(self.baseurl, view_item.id) + server_response = self.get_request(url, req_options) + pdf = server_response.content + return pdf + + @api(version="2.7") + def populate_csv(self, view_item, req_options=None): + if not view_item.id: + error = "View item missing ID." + raise MissingRequiredFieldError(error) + + def csv_fetcher(): + return self._get_view_csv(view_item, req_options) + + view_item._set_csv(csv_fetcher) + logger.info("Populated csv for view (ID: {0})".format(view_item.id)) + + def _get_view_csv(self, view_item, req_options): + url = "{0}/{1}/data".format(self.baseurl, view_item.id) + + with closing(self.get_request(url, parameters={"stream": True})) as server_response: + csv = server_response.iter_content(1024) + return csv # Update view. Currently only tags can be updated def update(self, view_item): diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index f86e848fc..4ce9983f3 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -4,6 +4,7 @@ from .resource_tagger import _ResourceTagger from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem from ...models.tag_item import TagItem +from ...models.job_item import JobItem from ...filesys_helpers import to_filename import os @@ -50,6 +51,14 @@ def get_by_id(self, workbook_id): server_response = self.get_request(url) return WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0] + @api(version="2.8") + def refresh(self, workbook_id): + url = "{0}/{1}/refresh".format(self.baseurl, workbook_id) + empty_req = RequestFactory.Empty.empty_req() + server_response = self.post_request(url, empty_req) + new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] + return new_job + # Delete 1 workbook by id @api(version="2.0") def delete(self, workbook_id): @@ -77,12 +86,23 @@ def update(self, workbook_item): updated_workbook = copy.copy(workbook_item) return updated_workbook._parse_common_tags(server_response.content, self.parent_srv.namespace) + @api(version="2.3") + def update_conn(self, *args, **kwargs): + import warnings + warnings.warn('update_conn is deprecated, please use update_connection instead') + return self.update_connection(*args, **kwargs) + # Update workbook_connection - def update_conn(self, workbook_item, connection_item): + @api(version="2.3") + def update_connection(self, workbook_item, connection_item): url = "{0}/{1}/connections/{2}".format(self.baseurl, workbook_item.id, connection_item.id) - update_req = RequestFactory.WorkbookConnection.update_req(connection_item) + update_req = RequestFactory.Connection.update_req(connection_item) server_response = self.put_request(url, update_req) - logger.info('Updated workbook item (ID: {0} & connection item {1}'.format(workbook_item.id, connection_item.id)) + connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] + + logger.info('Updated workbook item (ID: {0} & connection item {1}'.format(workbook_item.id, + connection_item.id)) + return connection # Download workbook contents with option of passing in filepath @api(version="2.0") @@ -118,16 +138,26 @@ def download(self, workbook_id, filepath=None, include_extract=True, no_extract= # Get all views of workbook @api(version="2.0") - def populate_views(self, workbook_item): + def populate_views(self, workbook_item, usage=False): if not workbook_item.id: error = "Workbook item missing ID. Workbook must be retrieved from server first." raise MissingRequiredFieldError(error) + + def view_fetcher(): + return self._get_views_for_workbook(workbook_item, usage) + + workbook_item._set_views(view_fetcher) + logger.info('Populated views for workbook (ID: {0}'.format(workbook_item.id)) + + def _get_views_for_workbook(self, workbook_item, usage): url = "{0}/{1}/views".format(self.baseurl, workbook_item.id) + if usage: + url += "?includeUsageStatistics=true" server_response = self.get_request(url) - workbook_item._set_views(ViewItem.from_response(server_response.content, - self.parent_srv.namespace, - workbook_id=workbook_item.id)) - logger.info('Populated views for workbook (ID: {0}'.format(workbook_item.id)) + views = ViewItem.from_response(server_response.content, + self.parent_srv.namespace, + workbook_id=workbook_item.id) + return views # Get all connections of workbook @api(version="2.0") @@ -135,21 +165,37 @@ def populate_connections(self, workbook_item): if not workbook_item.id: error = "Workbook item missing ID. Workbook must be retrieved from server first." raise MissingRequiredFieldError(error) - url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id) - server_response = self.get_request(url) - workbook_item._set_connections(ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)) + + def connection_fetcher(): + return self._get_workbook_connections(workbook_item) + + workbook_item._set_connections(connection_fetcher) logger.info('Populated connections for workbook (ID: {0})'.format(workbook_item.id)) + def _get_workbook_connections(self, workbook_item, req_options=None): + url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id) + server_response = self.get_request(url, req_options) + connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) + return connections + # Get preview image of workbook @api(version="2.0") def populate_preview_image(self, workbook_item): if not workbook_item.id: error = "Workbook item missing ID. Workbook must be retrieved from server first." raise MissingRequiredFieldError(error) + + def image_fetcher(): + return self._get_wb_preview_image(workbook_item) + + workbook_item._set_preview_image(image_fetcher) + logger.info('Populated preview image for workbook (ID: {0})'.format(workbook_item.id)) + + def _get_wb_preview_image(self, workbook_item): url = "{0}/{1}/previewImage".format(self.baseurl, workbook_item.id) server_response = self.get_request(url) - workbook_item._set_preview_image(server_response.content) - logger.info('Populated preview image for workbook (ID: {0})'.format(workbook_item.id)) + preview_image = server_response.content + return preview_image # Publishes workbook. Chunking method if file over 64MB @api(version="2.0") diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py index a53bd930f..78c927dda 100644 --- a/tableauserverclient/server/pager.py +++ b/tableauserverclient/server/pager.py @@ -47,7 +47,7 @@ def __iter__(self): except IndexError: # The total count on Server changed while fetching exit gracefully - raise StopIteration + return def _load_next_page(self, last_pagination_item): next_page = last_pagination_item.page_number + 1 diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 44bb6e749..12b0ce650 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -20,7 +20,7 @@ def _add_multipart(parts): def _tsrequest_wrapped(func): def wrapper(self, *args, **kwargs): xml_request = ET.Element('tsRequest') - func(xml_request, *args, **kwargs) + func(self, xml_request, *args, **kwargs) return ET.tostring(xml_request) return wrapper @@ -209,6 +209,29 @@ def update_req(self, schedule_item): single_interval_element.attrib[expression] = value return ET.tostring(xml_request) + def _add_to_req(self, id_, type_): + """ + + + + + + + """ + xml_request = ET.Element('tsRequest') + task_element = ET.SubElement(xml_request, 'task') + refresh = ET.SubElement(task_element, 'extractRefresh') + workbook = ET.SubElement(refresh, type_) + workbook.attrib['id'] = id_ + + return ET.tostring(xml_request) + + def add_workbook_req(self, id_): + return self._add_to_req(id_, "workbook") + + def add_datasource_req(self, id_): + return self._add_to_req(id_, "datasource") + class SiteRequest(object): def update_req(self, site_item): @@ -335,9 +358,9 @@ def publish_req_chunked(self, workbook_item, connection_credentials=None): return _add_multipart(parts) -class WorkbookConnection(object): - def update_req(self, connection_item): - xml_request = ET.Element('tsRequest') +class Connection(object): + @_tsrequest_wrapped + def update_req(self, xml_request, connection_item): connection_element = ET.SubElement(xml_request, 'connection') if connection_item.server_address: connection_element.attrib['serverAddress'] = connection_item.server_address.lower() @@ -348,20 +371,45 @@ def update_req(self, connection_item): if connection_item.password: connection_element.attrib['password'] = connection_item.password if connection_item.embed_password: - connection_element.attrib['embedPassword'] = connection_item.embed_password - return ET.tostring(xml_request) + connection_element.attrib['embedPassword'] = str(connection_item.embed_password) class TaskRequest(object): @_tsrequest_wrapped - def run_req(xml_request, task_item): + def run_req(self, xml_request, task_item): # Send an empty tsRequest pass +class SubscriptionRequest(object): + def create_req(self, subscription_item): + xml_request = ET.Element('tsRequest') + subscription_element = ET.SubElement(xml_request, 'subscription') + subscription_element.attrib['subject'] = subscription_item.subject + + content_element = ET.SubElement(subscription_element, 'content') + content_element.attrib['id'] = subscription_item.target.id + content_element.attrib['type'] = subscription_item.target.type + + schedule_element = ET.SubElement(subscription_element, 'schedule') + schedule_element.attrib['id'] = subscription_item.schedule_id + + user_element = ET.SubElement(subscription_element, 'user') + user_element.attrib['id'] = subscription_item.user_id + return ET.tostring(xml_request) + + +class EmptyRequest(object): + @_tsrequest_wrapped + def empty_req(xml_request): + pass + + class RequestFactory(object): Auth = AuthRequest() + Connection = Connection() Datasource = DatasourceRequest() + Empty = EmptyRequest() Fileupload = FileuploadRequest() Group = GroupRequest() Permission = PermissionRequest() @@ -372,4 +420,4 @@ class RequestFactory(object): Task = TaskRequest() User = UserRequest() Workbook = WorkbookRequest() - WorkbookConnection = WorkbookConnection() + Subscription = SubscriptionRequest() diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index acd82c68e..37f23f54c 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -41,6 +41,11 @@ def page_number(self, page_number): def apply_query_params(self, url): params = [] + + if '?' in url: + url, existing_params = url.split('?') + params.append(existing_params) + if self.page_number: params.append('pageNumber={0}'.format(self.pagenumber)) if self.page_size: @@ -63,35 +68,47 @@ class Resolution: High = 'high' def __init__(self, imageresolution=None): - self.imageresolution = imageresolution - - def image_resolution(self, imageresolution): - self.imageresolution = imageresolution - return self + self.image_resolution = imageresolution def apply_query_params(self, url): params = [] if self.image_resolution: - params.append('resolution={0}'.format(self.imageresolution)) + params.append('resolution={0}'.format(self.image_resolution)) return "{0}?{1}".format(url, '&'.join(params)) -class ImageRequestOptions(RequestOptionsBase): +class PDFRequestOptions(RequestOptionsBase): # if 'high' isn't specified, the REST API endpoint returns an image with standard resolution - class Resolution: - High = 'high' - - def __init__(self, imageresolution=None): - self.imageresolution = imageresolution - - def image_resolution(self, imageresolution): - self.imageresolution = imageresolution - return self + class PageType: + A3 = "a3" + A4 = "a4" + A5 = "a5" + B4 = "b4" + B5 = "b5" + Executive = "executive" + Folio = "folio" + Ledger = "ledger" + Legal = "legal" + Letter = "letter" + Note = "note" + Quarto = "quarto" + Tabloid = "tabloid" + + class Orientation: + Portrait = "portrait" + Landscape = "landscape" + + def __init__(self, page_type=None, orientation=None): + self.page_type = page_type + self.orientation = orientation def apply_query_params(self, url): params = [] - if self.image_resolution: - params.append('resolution={0}'.format(self.imageresolution)) + if self.page_type: + params.append('type={0}'.format(self.page_type)) + + if self.orientation: + params.append('orientation={0}'.format(self.orientation)) return "{0}?{1}".format(url, '&'.join(params)) diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 4a4bf0ac0..0c2b4f1c2 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -3,7 +3,7 @@ from .exceptions import NotSignedInError from ..namespace import Namespace from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \ - Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError + Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs import requests @@ -36,12 +36,14 @@ def __init__(self, server_address, use_server_version=False): self.users = Users(self) self.sites = Sites(self) self.groups = Groups(self) + self.jobs = Jobs(self) self.workbooks = Workbooks(self) self.datasources = Datasources(self) self.projects = Projects(self) self.schedules = Schedules(self) self.server_info = ServerInfo(self) self.tasks = Tasks(self) + self.subscriptions = Subscriptions(self) self._namespace = Namespace() if use_server_version: diff --git a/test/_utils.py b/test/_utils.py new file mode 100644 index 000000000..ecabf53a4 --- /dev/null +++ b/test/_utils.py @@ -0,0 +1,16 @@ +import os.path + +TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') + + +def asset(filename): + return os.path.join(TEST_ASSET_DIR, filename) + + +def read_xml_asset(filename): + with open(asset(filename), 'rb') as f: + return f.read().decode('utf-8') + + +def read_xml_assets(*args): + return map(read_xml_asset, args) diff --git a/test/assets/datasource_connection_update.xml b/test/assets/datasource_connection_update.xml new file mode 100644 index 000000000..0e4d21ed0 --- /dev/null +++ b/test/assets/datasource_connection_update.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/assets/datasource_populate_connections.xml b/test/assets/datasource_populate_connections.xml new file mode 100644 index 000000000..442a78323 --- /dev/null +++ b/test/assets/datasource_populate_connections.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/test/assets/populate_csv.csv b/test/assets/populate_csv.csv new file mode 100644 index 000000000..b12de4304 --- /dev/null +++ b/test/assets/populate_csv.csv @@ -0,0 +1,25 @@ +Measure Names,Region,Profit Ratio,Sales per Customer,Distinct count of Customer Name,Measure Values,Profit,Quantity,Sales +Count of Customers,South,14.4%,$711.83,438,438,"$45,047","5,004","$311,784" +Sales,South,14.4%,$711.83,438,"311,783.644","$45,047","5,004","$311,784" +Quantity,South,14.4%,$711.83,438,"5,004","$45,047","5,004","$311,784" +Sales per Customer,South,14.4%,$711.83,438,711.834803653,"$45,047","5,004","$311,784" +Profit,South,14.4%,$711.83,438,"45,047.2231","$45,047","5,004","$311,784" +Profit Ratio,South,14.4%,$711.83,438,0.144482316,"$45,047","5,004","$311,784" +Count of Customers,Central,9.3%,$746.66,566,566,"$39,176","6,990","$422,611" +Sales,Central,9.3%,$746.66,566,"422,610.558800001","$39,176","6,990","$422,611" +Quantity,Central,9.3%,$746.66,566,"6,990","$39,176","6,990","$422,611" +Sales per Customer,Central,9.3%,$746.66,566,746.661764664,"$39,176","6,990","$422,611" +Profit,Central,9.3%,$746.66,566,"39,176.1836","$39,176","6,990","$422,611" +Profit Ratio,Central,9.3%,$746.66,566,0.092700437,"$39,176","6,990","$422,611" +Count of Customers,East,12.7%,$825.74,624,624,"$65,476","8,255","$515,262" +Sales,East,12.7%,$825.74,624,"515,261.598000001","$65,476","8,255","$515,262" +Quantity,East,12.7%,$825.74,624,"8,255","$65,476","8,255","$515,262" +Sales per Customer,East,12.7%,$825.74,624,825.739740385,"$65,476","8,255","$515,262" +Profit,East,12.7%,$825.74,624,"65,475.852700000","$65,476","8,255","$515,262" +Profit Ratio,East,12.7%,$825.74,624,0.127073030,"$65,476","8,255","$515,262" +Count of Customers,West,14.4%,$906.73,630,630,"$82,264","9,544","$571,239" +Sales,West,14.4%,$906.73,630,"571,239.036500001","$82,264","9,544","$571,239" +Quantity,West,14.4%,$906.73,630,"9,544","$82,264","9,544","$571,239" +Sales per Customer,West,14.4%,$906.73,630,906.728629365,"$82,264","9,544","$571,239" +Profit,West,14.4%,$906.73,630,"82,263.903800000","$82,264","9,544","$571,239" +Profit Ratio,West,14.4%,$906.73,630,0.144009598,"$82,264","9,544","$571,239" diff --git a/test/assets/populate_pdf.pdf b/test/assets/populate_pdf.pdf new file mode 100644 index 000000000..4d3319442 Binary files /dev/null and b/test/assets/populate_pdf.pdf differ diff --git a/test/assets/subscription_get.xml b/test/assets/subscription_get.xml new file mode 100644 index 000000000..d038c8419 --- /dev/null +++ b/test/assets/subscription_get.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/subscription_get_by_id.xml b/test/assets/subscription_get_by_id.xml new file mode 100644 index 000000000..0677da63d --- /dev/null +++ b/test/assets/subscription_get_by_id.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/test/assets/view_get_usage.xml b/test/assets/view_get_usage.xml new file mode 100644 index 000000000..a6844879d --- /dev/null +++ b/test/assets/view_get_usage.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/workbook_populate_views_usage.xml b/test/assets/workbook_populate_views_usage.xml new file mode 100644 index 000000000..a75e4037f --- /dev/null +++ b/test/assets/workbook_populate_views_usage.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_datasource.py b/test/test_datasource.py index 3b9b29248..ff1546d62 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -3,15 +3,16 @@ import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime +from ._utils import read_xml_asset, read_xml_assets, asset -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') - -ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'datasource_add_tags.xml') -GET_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get.xml') -GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_empty.xml') -GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_by_id.xml') -PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'datasource_publish.xml') -UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'datasource_update.xml') +ADD_TAGS_XML = 'datasource_add_tags.xml' +GET_XML = 'datasource_get.xml' +GET_EMPTY_XML = 'datasource_get_empty.xml' +GET_BY_ID_XML = 'datasource_get_by_id.xml' +POPULATE_CONNECTIONS_XML = 'datasource_populate_connections.xml' +PUBLISH_XML = 'datasource_publish.xml' +UPDATE_XML = 'datasource_update.xml' +UPDATE_CONNECTION_XML = 'datasource_connection_update.xml' class DatasourceTests(unittest.TestCase): @@ -25,8 +26,7 @@ def setUp(self): self.baseurl = self.server.datasources.baseurl def test_get(self): - with open(GET_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(GET_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) all_datasources, pagination_item = self.server.datasources.get() @@ -58,8 +58,7 @@ def test_get_before_signin(self): self.assertRaises(TSC.NotSignedInError, self.server.datasources.get) def test_get_empty(self): - with open(GET_EMPTY_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(GET_EMPTY_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) all_datasources, pagination_item = self.server.datasources.get() @@ -68,8 +67,7 @@ def test_get_empty(self): self.assertEqual([], all_datasources) def test_get_by_id(self): - with open(GET_BY_ID_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(GET_BY_ID_XML) with requests_mock.mock() as m: m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml) single_datasource = self.server.datasources.get_by_id('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb') @@ -86,8 +84,7 @@ def test_get_by_id(self): self.assertEqual(set(['world', 'indicators', 'sample']), single_datasource.tags) def test_update(self): - with open(UPDATE_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(UPDATE_XML) with requests_mock.mock() as m: m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml) single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74') @@ -104,7 +101,7 @@ def test_update(self): self.assertEqual("Warning, here be dragons.", single_datasource.certification_note) def test_update_copy_fields(self): - with open(UPDATE_XML, 'rb') as f: + with open(asset(UPDATE_XML), 'rb') as f: response_xml = f.read().decode('utf-8') with requests_mock.mock() as m: m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', text=response_xml) @@ -117,10 +114,7 @@ def test_update_copy_fields(self): self.assertEqual(single_datasource._project_name, updated_datasource._project_name) def test_update_tags(self): - with open(ADD_TAGS_XML, 'rb') as f: - add_tags_xml = f.read().decode('utf-8') - with open(UPDATE_XML, 'rb') as f: - update_xml = f.read().decode('utf-8') + add_tags_xml, update_xml = read_xml_assets(ADD_TAGS_XML, UPDATE_XML) with requests_mock.mock() as m: m.put(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags', text=add_tags_xml) m.delete(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/b', status_code=204) @@ -135,14 +129,51 @@ def test_update_tags(self): self.assertEqual(single_datasource.tags, updated_datasource.tags) self.assertEqual(single_datasource._initial_tags, updated_datasource._initial_tags) + def test_populate_connections(self): + response_xml = read_xml_asset(POPULATE_CONNECTIONS_XML) + with requests_mock.mock() as m: + m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections', text=response_xml) + single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74') + single_datasource.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + single_datasource._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb' + self.server.datasources.populate_connections(single_datasource) + + self.assertEqual('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', single_datasource.id) + + connections = single_datasource.connections + self.assertTrue(connections) + ds1, ds2, ds3 = connections + self.assertEqual(ds1.id, 'be786ae0-d2bf-4a4b-9b34-e2de8d2d4488') + self.assertEqual(ds2.id, '970e24bc-e200-4841-a3e9-66e7d122d77e') + self.assertEqual(ds3.id, '7d85b889-283b-42df-b23e-3c811e402f1f') + + def test_update_connection(self): + populate_xml, response_xml = read_xml_assets(POPULATE_CONNECTIONS_XML, UPDATE_CONNECTION_XML) + + with requests_mock.mock() as m: + m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections', text=populate_xml) + m.put(self.baseurl + + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/be786ae0-d2bf-4a4b-9b34-e2de8d2d4488', + text=response_xml) + single_datasource = TSC.DatasourceItem('test', '1d0304cd-3796-429f-b815-7258370b9b74') + single_datasource.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + single_datasource._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb' + self.server.datasources.populate_connections(single_datasource) + + connection = single_datasource.connections[0] + connection.username = 'foo' + new_connection = self.server.datasources.update_connection(single_datasource, connection) + self.assertEqual(connection.id, new_connection.id) + self.assertEqual(connection.connection_type, new_connection.connection_type) + self.assertEqual('foo', new_connection.username) + def test_publish(self): - with open(PUBLISH_XML, 'rb') as f: - response_xml = f.read().decode('utf-8') + response_xml = read_xml_asset(PUBLISH_XML) with requests_mock.mock() as m: m.post(self.baseurl, text=response_xml) new_datasource = TSC.DatasourceItem('SampleDS', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') new_datasource = self.server.datasources.publish(new_datasource, - os.path.join(TEST_ASSET_DIR, 'SampleDS.tds'), + asset('SampleDS.tds'), mode=self.server.PublishMode.CreateNew) self.assertEqual('e76a1461-3b1d-4588-bf1b-17551a879ad9', new_datasource.id) @@ -204,9 +235,9 @@ def test_publish_missing_path(self): def test_publish_missing_mode(self): new_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') self.assertRaises(ValueError, self.server.datasources.publish, new_datasource, - os.path.join(TEST_ASSET_DIR, 'SampleDS.tds'), None) + asset('SampleDS.tds'), None) def test_publish_invalid_file_type(self): new_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') self.assertRaises(ValueError, self.server.datasources.publish, new_datasource, - os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx'), self.server.PublishMode.Append) + asset('SampleWB.twbx'), self.server.PublishMode.Append) diff --git a/test/test_schedule.py b/test/test_schedule.py index 965e414a8..a9ae9bb67 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -15,6 +15,9 @@ CREATE_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_monthly.xml") UPDATE_XML = os.path.join(TEST_ASSET_DIR, "schedule_update.xml") +WORKBOOK_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'workbook_get_by_id.xml') +DATASOURCE_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, 'datasource_get_by_id.xml') + class ScheduleTests(unittest.TestCase): def setUp(self): @@ -182,3 +185,31 @@ def test_update(self): self.assertEqual(time(7), single_schedule.interval_item.start_time) self.assertEqual(("Monday", "Friday"), single_schedule.interval_item.interval) + + def test_add_workbook(self): + self.server.version = "2.8" + baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + + with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: + workbook_response = f.read().decode("utf-8") + with requests_mock.mock() as m: + # TODO: Replace with real response + m.get(self.server.workbooks.baseurl + '/bar', text=workbook_response) + m.put(baseurl + '/foo/workbooks', text="OK") + workbook = self.server.workbooks.get_by_id("bar") + result = self.server.schedules.add_to_schedule('foo', workbook=workbook) + self.assertEqual(0, len(result), "Added properly") + + def test_add_datasource(self): + self.server.version = "2.8" + baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + + with open(DATASOURCE_GET_BY_ID_XML, "rb") as f: + datasource_response = f.read().decode("utf-8") + with requests_mock.mock() as m: + # TODO: Replace with real response + m.get(self.server.datasources.baseurl + '/bar', text=datasource_response) + m.put(baseurl + '/foo/datasources', text="OK") + datasource = self.server.datasources.get_by_id("bar") + result = self.server.schedules.add_to_schedule('foo', datasource=datasource) + self.assertEqual(0, len(result), "Added properly") diff --git a/test/test_server_info.py b/test/test_server_info.py index 2eb763a80..3dadff7c1 100644 --- a/test/test_server_info.py +++ b/test/test_server_info.py @@ -39,7 +39,7 @@ def test_server_info_use_highest_version_downgrades(self): # 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.server.use_server_version() self.assertEqual(self.server.version, '2.2') def test_server_info_use_highest_version_upgrades(self): @@ -49,7 +49,7 @@ def test_server_info_use_highest_version_upgrades(self): 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() + self.server.use_server_version() # Did we upgrade to 2.4? self.assertEqual(self.server.version, '2.4') diff --git a/test/test_subscription.py b/test/test_subscription.py new file mode 100644 index 000000000..50fc7046f --- /dev/null +++ b/test/test_subscription.py @@ -0,0 +1,50 @@ +import unittest +import os +import requests_mock +import tableauserverclient as TSC + +TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") + +GET_XML = os.path.join(TEST_ASSET_DIR, "subscription_get.xml") +GET_XML_BY_ID = os.path.join(TEST_ASSET_DIR, "subscription_get_by_id.xml") + + +class SubscriptionTests(unittest.TestCase): + def setUp(self): + self.server = TSC.Server("http://test") + self.server.version = '2.6' + + # Fake Signin + self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + self.baseurl = self.server.subscriptions.baseurl + + def test_get_subscriptions(self): + with open(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) + all_subscriptions, pagination_item = self.server.subscriptions.get() + + subscription = all_subscriptions[0] + self.assertEqual('382e9a6e-0c08-4a95-b6c1-c14df7bac3e4', subscription.id) + self.assertEqual('View', subscription.target.type) + self.assertEqual('cdd716ca-5818-470e-8bec-086885dbadee', subscription.target.id) + self.assertEqual('c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e', subscription.user_id) + self.assertEqual('Not Found Alert', subscription.subject) + self.assertEqual('7617c389-cdca-4940-a66e-69956fcebf3e', subscription.schedule_id) + + def test_get_subscription_by_id(self): + with open(GET_XML_BY_ID, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.get(self.baseurl + '/382e9a6e-0c08-4a95-b6c1-c14df7bac3e4', text=response_xml) + subscription = self.server.subscriptions.get_by_id('382e9a6e-0c08-4a95-b6c1-c14df7bac3e4') + + self.assertEqual('382e9a6e-0c08-4a95-b6c1-c14df7bac3e4', subscription.id) + self.assertEqual('View', subscription.target.type) + self.assertEqual('cdd716ca-5818-470e-8bec-086885dbadee', subscription.target.id) + self.assertEqual('c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e', subscription.user_id) + self.assertEqual('Not Found Alert', subscription.subject) + self.assertEqual('7617c389-cdca-4940-a66e-69956fcebf3e', subscription.schedule_id) diff --git a/test/test_user.py b/test/test_user.py index fa8344371..8df2f2b2e 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -128,21 +128,20 @@ def test_populate_workbooks(self): text=response_xml) single_user = TSC.UserItem('test', 'Interactor') single_user._id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' - pagination_item = self.server.users.populate_workbooks(single_user) - - workbook_list = single_user.workbooks - self.assertEqual(1, pagination_item.total_available) - self.assertEqual('3cc6cd06-89ce-4fdc-b935-5294135d6d42', workbook_list[0].id) - self.assertEqual('SafariSample', workbook_list[0].name) - 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', 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) - self.assertEqual(set(['Safari', 'Sample']), workbook_list[0].tags) + self.server.users.populate_workbooks(single_user) + + workbook_list = list(single_user.workbooks) + self.assertEqual('3cc6cd06-89ce-4fdc-b935-5294135d6d42', workbook_list[0].id) + self.assertEqual('SafariSample', workbook_list[0].name) + 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', 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) + self.assertEqual(set(['Safari', 'Sample']), workbook_list[0].tags) def test_populate_workbooks_missing_id(self): single_user = TSC.UserItem('test', 'Interactor') diff --git a/test/test_view.py b/test/test_view.py index b1340ff60..09ce2f3d7 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -7,13 +7,17 @@ ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'view_add_tags.xml') GET_XML = os.path.join(TEST_ASSET_DIR, 'view_get.xml') +GET_XML_USAGE = os.path.join(TEST_ASSET_DIR, 'view_get_usage.xml') POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'Sample View Image.png') +POPULATE_PDF = os.path.join(TEST_ASSET_DIR, 'populate_pdf.pdf') +POPULATE_CSV = os.path.join(TEST_ASSET_DIR, 'populate_csv.csv') UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml') class ViewTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') + self.server.version = '2.7' # Fake sign in self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' @@ -42,6 +46,33 @@ def test_get(self): self.assertEqual('6d13b0ca-043d-4d42-8c9d-3f3313ea3a00', all_views[1].workbook_id) self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', all_views[1].owner_id) + def test_get_with_usage(self): + with open(GET_XML_USAGE, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.baseurl + "?includeUsageStatistics=true", text=response_xml) + all_views, pagination_item = self.server.views.get(usage=True) + + self.assertEqual('d79634e1-6063-4ec9-95ff-50acbf609ff5', all_views[0].id) + self.assertEqual(7, all_views[0].total_views) + self.assertEqual('fd252f73-593c-4c4e-8584-c032b8022adc', all_views[1].id) + self.assertEqual(13, all_views[1].total_views) + + def test_get_with_usage_and_filter(self): + with open(GET_XML_USAGE, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.baseurl + "?includeUsageStatistics=true&filter=name:in:[foo,bar]", text=response_xml) + options = TSC.RequestOptions() + options.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, + ["foo", "bar"])) + all_views, pagination_item = self.server.views.get(req_options=options, usage=True) + + self.assertEqual("ENDANGERED SAFARI", all_views[0].name) + self.assertEqual(7, all_views[0].total_views) + self.assertEqual("Overview", all_views[1].name) + self.assertEqual(13, all_views[1].total_views) + def test_get_before_signin(self): self.server._auth_token = None self.assertRaises(TSC.NotSignedInError, self.server.views.get) @@ -56,7 +87,7 @@ def test_populate_preview_image(self): single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' single_view._workbook_id = '3cc6cd06-89ce-4fdc-b935-5294135d6d42' self.server.views.populate_preview_image(single_view) - self.assertEqual(response, single_view.preview_image) + self.assertEqual(response, single_view.preview_image) def test_populate_preview_image_missing_id(self): single_view = TSC.ViewItem() @@ -75,7 +106,7 @@ def test_populate_image(self): single_view = TSC.ViewItem() single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' self.server.views.populate_image(single_view) - self.assertEqual(response, single_view.image) + self.assertEqual(response, single_view.image) def test_populate_image_high_resolution(self): with open(POPULATE_PREVIEW_IMAGE, 'rb') as f: @@ -86,7 +117,35 @@ def test_populate_image_high_resolution(self): single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High) self.server.views.populate_image(single_view, req_option) - self.assertEqual(response, single_view.image) + self.assertEqual(response, single_view.image) + + def test_populate_pdf(self): + with open(POPULATE_PDF, 'rb') as f: + response = f.read() + with requests_mock.mock() as m: + m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait', + content=response) + single_view = TSC.ViewItem() + single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + + size = TSC.PDFRequestOptions.PageType.Letter + orientation = TSC.PDFRequestOptions.Orientation.Portrait + req_option = TSC.PDFRequestOptions(size, orientation) + + self.server.views.populate_pdf(single_view, req_option) + self.assertEqual(response, single_view.pdf) + + def test_populate_csv(self): + with open(POPULATE_CSV, 'rb') as f: + response = f.read() + with requests_mock.mock() as m: + m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5/data', content=response) + single_view = TSC.ViewItem() + single_view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + self.server.views.populate_csv(single_view) + + csv_file = b"".join(single_view.csv) + self.assertEqual(response, csv_file) def test_populate_image_missing_id(self): single_view = TSC.ViewItem() diff --git a/test/test_workbook.py b/test/test_workbook.py index 7a2131e7f..8c36f0229 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -13,6 +13,7 @@ POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_connections.xml') POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'RESTAPISample Image.png') POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views.xml') +POPULATE_VIEWS_USAGE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_populate_views_usage.xml') PUBLISH_XML = os.path.join(TEST_ASSET_DIR, 'workbook_publish.xml') UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'workbook_update.xml') @@ -207,18 +208,36 @@ def test_populate_views(self): single_workbook._id = '1f951daf-4061-451a-9df1-69a8062664f2' self.server.workbooks.populate_views(single_workbook) - views_list = single_workbook.views - self.assertEqual('097dbe13-de89-445f-b2c3-02f28bd010c1', views_list[0].id) - self.assertEqual('GDP per capita', views_list[0].name) - self.assertEqual('RESTAPISample/sheets/GDPpercapita', views_list[0].content_url) + views_list = single_workbook.views + self.assertEqual('097dbe13-de89-445f-b2c3-02f28bd010c1', views_list[0].id) + self.assertEqual('GDP per capita', views_list[0].name) + self.assertEqual('RESTAPISample/sheets/GDPpercapita', views_list[0].content_url) - self.assertEqual('2c1ab9d7-8d64-4cc6-b495-52e40c60c330', views_list[1].id) - self.assertEqual('Country ranks', views_list[1].name) - self.assertEqual('RESTAPISample/sheets/Countryranks', views_list[1].content_url) + self.assertEqual('2c1ab9d7-8d64-4cc6-b495-52e40c60c330', views_list[1].id) + self.assertEqual('Country ranks', views_list[1].name) + self.assertEqual('RESTAPISample/sheets/Countryranks', views_list[1].content_url) - self.assertEqual('0599c28c-6d82-457e-a453-e52c1bdb00f5', views_list[2].id) - self.assertEqual('Interest rates', views_list[2].name) - self.assertEqual('RESTAPISample/sheets/Interestrates', views_list[2].content_url) + self.assertEqual('0599c28c-6d82-457e-a453-e52c1bdb00f5', views_list[2].id) + self.assertEqual('Interest rates', views_list[2].name) + self.assertEqual('RESTAPISample/sheets/Interestrates', views_list[2].content_url) + + def test_populate_views_with_usage(self): + with open(POPULATE_VIEWS_USAGE_XML, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.baseurl + '/1f951daf-4061-451a-9df1-69a8062664f2/views?includeUsageStatistics=true', + text=response_xml) + single_workbook = TSC.WorkbookItem('test') + single_workbook._id = '1f951daf-4061-451a-9df1-69a8062664f2' + self.server.workbooks.populate_views(single_workbook, usage=True) + + views_list = single_workbook.views + self.assertEqual('097dbe13-de89-445f-b2c3-02f28bd010c1', views_list[0].id) + self.assertEqual(2, views_list[0].total_views) + self.assertEqual('2c1ab9d7-8d64-4cc6-b495-52e40c60c330', views_list[1].id) + self.assertEqual(37, views_list[1].total_views) + self.assertEqual('0599c28c-6d82-457e-a453-e52c1bdb00f5', views_list[2].id) + self.assertEqual(0, views_list[2].total_views) def test_populate_views_missing_id(self): single_workbook = TSC.WorkbookItem('test') @@ -233,10 +252,10 @@ def test_populate_connections(self): single_workbook._id = '1f951daf-4061-451a-9df1-69a8062664f2' self.server.workbooks.populate_connections(single_workbook) - self.assertEqual('37ca6ced-58d7-4dcf-99dc-f0a85223cbef', single_workbook.connections[0].id) - self.assertEqual('dataengine', single_workbook.connections[0].connection_type) - self.assertEqual('4506225a-0d32-4ab1-82d3-c24e85f7afba', single_workbook.connections[0].datasource_id) - self.assertEqual('World Indicators', single_workbook.connections[0].datasource_name) + self.assertEqual('37ca6ced-58d7-4dcf-99dc-f0a85223cbef', single_workbook.connections[0].id) + self.assertEqual('dataengine', single_workbook.connections[0].connection_type) + self.assertEqual('4506225a-0d32-4ab1-82d3-c24e85f7afba', single_workbook.connections[0].datasource_id) + self.assertEqual('World Indicators', single_workbook.connections[0].datasource_name) def test_populate_connections_missing_id(self): single_workbook = TSC.WorkbookItem('test') @@ -253,7 +272,7 @@ def test_populate_preview_image(self): single_workbook._id = '1f951daf-4061-451a-9df1-69a8062664f2' self.server.workbooks.populate_preview_image(single_workbook) - self.assertEqual(response, single_workbook.preview_image) + self.assertEqual(response, single_workbook.preview_image) def test_populate_preview_image_missing_id(self): single_workbook = TSC.WorkbookItem('test')