From b35bfb50d3352af03c36b03ee9d6114b09dbd2f4 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Thu, 21 Sep 2017 08:32:51 -0700 Subject: [PATCH 1/8] Update Change Log --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a95d31b..c4fece541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ -## 0.5 (11 Sept 2017) +## 0.5.1 (21 Sept 2017 + +* Fix a critical issue caused by #224 that was the result of lack of test coverage (#226) + +## 0.5 (20 Sept 2017) * Added revision settings to update site (#187) * Added support for certified data sources (#189) From 39f53fa230a5f88b46328ad43a8c774e471eb10f Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 21 Oct 2017 00:12:34 -0700 Subject: [PATCH 2/8] Add refresh and jobs endpoints --- tableauserverclient/__init__.py | 2 +- tableauserverclient/models/__init__.py | 1 + tableauserverclient/models/job_item.py | 60 +++++++++++++++++++ tableauserverclient/server/__init__.py | 2 +- .../server/endpoint/__init__.py | 1 + .../server/endpoint/datasources_endpoint.py | 8 +++ .../server/endpoint/endpoint.py | 2 +- .../server/endpoint/jobs_endpoint.py | 19 ++++++ .../server/endpoint/workbooks_endpoint.py | 9 +++ tableauserverclient/server/request_factory.py | 5 ++ tableauserverclient/server/server.py | 3 +- 11 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 tableauserverclient/models/job_item.py create mode 100644 tableauserverclient/server/endpoint/jobs_endpoint.py diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 7e94583df..c5840d7b6 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,6 +1,6 @@ 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, \ SubscriptionItem diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index a593b9d56..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 diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py new file mode 100644 index 000000000..b5f36d086 --- /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 list(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/server/__init__.py b/tableauserverclient/server/__init__.py index 9dde13324..12a640723 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -2,7 +2,7 @@ from .request_options import ImageRequestOptions, PDFRequestOptions, RequestOptions from .filter import Filter from .sort import Sort -from .. import ConnectionItem, DatasourceItem,\ +from .. import ConnectionItem, DatasourceItem, JobItem, \ GroupItem, PaginationItem, ProjectItem, ScheduleItem, SiteItem, TableauAuth,\ UserItem, ViewItem, WorkbookItem, TaskItem, SubscriptionItem from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \ diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index 58f603b54..c75fe8519 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -3,6 +3,7 @@ from .endpoint import Endpoint from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError from .groups_endpoint import Groups +from .jobs_endpoint import Jobs from .projects_endpoint import Projects from .schedules_endpoint import Schedules from .server_info_endpoint import ServerInfo diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 882001f18..5abfeb075 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -5,6 +5,7 @@ from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem from ...filesys_helpers import to_filename from ...models.tag_item import TagItem +from ...models.job_item import JobItem import os import logging import copy @@ -128,6 +129,13 @@ def update(self, datasource_item): updated_datasource = copy.copy(datasource_item) return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace) + def refresh(self, datasource_item): + url = "{0}/{1}/refresh".format(self.baseurl, datasource_item.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 + # Publish datasource @api(version="2.0") def publish(self, datasource_item, file_path, mode, connection_credentials=None): diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index cdc52e4ce..db5c6c1d2 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -10,7 +10,7 @@ logger = logging.getLogger('tableau.endpoint') -Success_codes = [200, 201, 204] +Success_codes = [200, 201, 202, 204] class Endpoint(object): diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py new file mode 100644 index 000000000..19d3f3505 --- /dev/null +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -0,0 +1,19 @@ +from .endpoint import Endpoint, api +from .. import JobItem +import logging + +logger = logging.getLogger('tableau.endpoint.jobs') + + +class Jobs(Endpoint): + @property + def baseurl(self): + return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id) + + @api(version='2.6') + def get(self, job_id): + logger.info('Query for information about job ' + job_id) + url = "{0}/{1}".format(self.baseurl, job_id) + server_response = self.get_request(url) + new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] + return new_job \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index ec5cedc22..f9ddd6f37 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): diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 464971472..239667cbc 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -376,10 +376,15 @@ def create_req(self, subscription_item): 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() Datasource = DatasourceRequest() + Empty = EmptyRequest() Fileupload = FileuploadRequest() Group = GroupRequest() Permission = PermissionRequest() diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index eee37f483..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, Subscriptions + Schedules, ServerInfo, Tasks, ServerInfoEndpointNotFoundError, Subscriptions, Jobs import requests @@ -36,6 +36,7 @@ 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) From 11f17becf81c07cedc9520110b052a8157fcba30 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 11:30:31 -0800 Subject: [PATCH 3/8] Fixing pycodestyle failures --- tableauserverclient/models/job_item.py | 2 +- tableauserverclient/server/endpoint/jobs_endpoint.py | 2 +- tableauserverclient/server/request_factory.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index b5f36d086..89a7c5303 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -3,7 +3,7 @@ class JobItem(object): - def __init__(self, id_, job_type, created_at, started_at = None, completed_at = None, finish_code = 0): + 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 diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index 19d3f3505..243b04d63 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -16,4 +16,4 @@ def get(self, job_id): url = "{0}/{1}".format(self.baseurl, job_id) server_response = self.get_request(url) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] - return new_job \ No newline at end of file + return new_job diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 239667cbc..a3f8fbbc2 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -376,11 +376,13 @@ def create_req(self, subscription_item): 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() Datasource = DatasourceRequest() From 5557d2b07f88dc314465acca5a39eac06afc65ce Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 11:57:22 -0800 Subject: [PATCH 4/8] Run pycodestyle on samples also --- .travis.yml | 2 +- samples/list.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 samples/list.py 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/samples/list.py b/samples/list.py new file mode 100644 index 000000000..e69de29bb From 8e1219a808d2228104fb497a69b48022db751ba7 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 11:57:51 -0800 Subject: [PATCH 5/8] Add sample for refresh and a useful tool to get the id for datasources and workbooks --- samples/refresh.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 samples/refresh.py 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() From 394cc6b84fb5b2d212b715e988a759100ec93d18 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 11:59:02 -0800 Subject: [PATCH 6/8] Actually checking in list.py this time --- samples/create_project.py | 1 + samples/list.py | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/samples/create_project.py b/samples/create_project.py index c68b992a9..d0c82001c 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -64,5 +64,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/list.py b/samples/list.py index e69de29bb..ec2ff9a6b 100644 --- a/samples/list.py +++ 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() From d8738c5e307282d9bc62414e8aae6332459b2179 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 12:09:04 -0800 Subject: [PATCH 7/8] more pep8 fixes --- samples/create_project.py | 1 + samples/download_view_image.py | 1 + samples/explore_datasource.py | 1 + samples/filter_sort_groups.py | 1 + 4 files changed, 4 insertions(+) diff --git a/samples/create_project.py b/samples/create_project.py index d0c82001c..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') 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() From 5197431b5fe8e0cf2f4df434c221e0f85b642418 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 13:01:32 -0800 Subject: [PATCH 8/8] Addressing Tyler's feedback --- tableauserverclient/models/job_item.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 89a7c5303..cc53765ed 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -45,16 +45,16 @@ def from_response(cls, xml, ns): all_tasks_xml = parsed_response.findall( './/t:job', namespaces=ns) - all_tasks = (JobItem._parse_element(x, ns) for x in all_tasks_xml) + all_tasks = [JobItem._parse_element(x, ns) for x in all_tasks_xml] - return list(all_tasks) + return all_tasks @classmethod def _parse_element(cls, element, ns): id_ = element.get('id', None) - type = element.get('type', 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) + return cls(id_, type_, created_at, started_at, completed_at, finish_code)