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) 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/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index cdc52e4ce..052eb6f68 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -1,4 +1,4 @@ -from .exceptions import ServerResponseError, EndpointUnavailableError +from .exceptions import ServerResponseError, EndpointUnavailableError, ItemTypeNotAllowed from functools import wraps import logging diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 48002de1b..d77cdea3e 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -31,3 +31,7 @@ class ServerInfoEndpointNotFoundError(Exception): class EndpointUnavailableError(Exception): pass + + +class ItemTypeNotAllowed(Exception): + pass diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 45f36d5a3..2de488bdb 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,10 +1,14 @@ from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, PaginationItem, ScheduleItem +from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem import logging import copy +from collections import namedtuple logger = logging.getLogger('tableau.endpoint.schedules') +# Oh to have a first class Result concept in Python... +AddResponse = namedtuple('AddResponse', ('result', 'error')) +OK = AddResponse(result=True, error=None) class Schedules(Endpoint): @@ -12,6 +16,10 @@ class Schedules(Endpoint): def baseurl(self): return "{0}/schedules".format(self.parent_srv.baseurl) + @property + def siteurl(self): + return "{0}/sites/{1}/schedules".format(self.parent_srv.baseurl, self.parent_srv.site_id) + @api(version="2.3") def get(self, req_options=None): logger.info("Querying all schedules") @@ -58,3 +66,28 @@ def create(self, schedule_item): new_schedule = ScheduleItem.from_response(server_response.content, self.parent_srv.namespace)[0] logger.info("Created new schedule (ID: {})".format(new_schedule.id)) return new_schedule + + @api(version="2.8") + def add_to_schedule(self, schedule_id, workbook=None, datasource=None): + + def add_to(resource, type_, req_factory): + id_ = resource.id + url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) + add_req = req_factory(id_) + response = self.put_request(url, add_req) + if response.status_code < 200 or response.status_code >= 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/request_factory.py b/tableauserverclient/server/request_factory.py index 464971472..66530f72d 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -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): diff --git a/test/test_schedule.py b/test/test_schedule.py index 965e414a8..59f6f8915 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.assertEquals(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.assertEquals(0, len(result), "Added properly")