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 ad9a14ddb1132e4ee11e5212d29e8d750406c261 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Wed, 15 Nov 2017 10:27:23 -0800 Subject: [PATCH 2/8] initial commit --- samples/set_refresh_schedule.py | 74 +++++++++++++++++++ .../server/endpoint/schedules_endpoint.py | 18 +++++ tableauserverclient/server/request_factory.py | 17 +++++ 3 files changed, 109 insertions(+) create mode 100644 samples/set_refresh_schedule.py diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py new file mode 100644 index 000000000..7b680c1b6 --- /dev/null +++ b/samples/set_refresh_schedule.py @@ -0,0 +1,74 @@ +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) + parser.add_argument('workbook') + 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_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_workbook_to_schedule(server, workbook, schedule): + retval = server.schedules.add_workbook(schedule.id, workbook.id) + print(retval) + + +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): + workbook = get_workbook_by_name(server, args.workbook) + schedule = get_schedule_by_name(server, args.schedule) + + assign_workbook_to_schedule(server, workbook, schedule) + + +def main(): + import sys + args = usage(sys.argv[1:]) + run(args) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 45f36d5a3..0afee6702 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -12,6 +12,11 @@ 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") @@ -21,6 +26,7 @@ def get(self, req_options=None): all_schedule_items = ScheduleItem.from_response(server_response.content, self.parent_srv.namespace) return all_schedule_items, pagination_item + @api(version="2.3") def delete(self, schedule_id): if not schedule_id: @@ -30,6 +36,7 @@ def delete(self, schedule_id): self.delete_request(url) logger.info("Deleted single schedule (ID: {0})".format(schedule_id)) + @api(version="2.3") def update(self, schedule_item): if not schedule_item.id: @@ -46,6 +53,7 @@ def update(self, schedule_item): updated_schedule = copy.copy(schedule_item) return updated_schedule._parse_common_tags(server_response.content, self.parent_srv.namespace) + @api(version="2.3") def create(self, schedule_item): if schedule_item.interval_item is None: @@ -58,3 +66,13 @@ 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_workbook(self, schedule_id, workbook_id): + url = "{0}/{1}/workbooks".format(self.siteurl, schedule_id) + print(url) + add_req = RequestFactory.Schedule.add_workbook_req(workbook_id) + server_response = self.put_request(url, add_req) + logger.info("Added workbook {} to schedule {}".format(workbook_id, schedule_id)) + print(server_response.content) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 464971472..ccb89b701 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -209,6 +209,23 @@ def update_req(self, schedule_item): single_interval_element.attrib[expression] = value return ET.tostring(xml_request) + def add_workbook_req(self, workbook_id): + """ + + + + + + + """ + xml_request = ET.Element('tsRequest') + task_element = ET.SubElement(xml_request, 'task') + refresh = ET.SubElement(task_element, 'extractRefresh') + workbook = ET.SubElement(refresh, "workbook") + workbook.attrib['id'] = workbook_id + + return ET.tostring(xml_request) + class SiteRequest(object): def update_req(self, site_item): From 1cdde32b54ae6a25bc3a7a1f59a8b6a41c8715ad Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Thu, 16 Nov 2017 08:57:17 -0800 Subject: [PATCH 3/8] Making the code more generic so it works with workbooks or datasources --- samples/set_refresh_schedule.py | 11 +++++---- .../server/endpoint/schedules_endpoint.py | 24 ++++++++++++++----- tableauserverclient/type_helpers.py | 2 ++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 tableauserverclient/type_helpers.py diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index 7b680c1b6..4f1c7278a 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -12,7 +12,9 @@ def usage(args): 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) - parser.add_argument('workbook') + 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) @@ -39,9 +41,8 @@ def get_schedule_by_name(server, name): return schedules.pop() -def assign_workbook_to_schedule(server, workbook, schedule): - retval = server.schedules.add_workbook(schedule.id, workbook.id) - print(retval) +def assign_to_schedule(server, workbook_or_datasource, schedule): + retval = server.schedules.add_to_schedule(schedule.id, workbook_or_datasource) def run(args): @@ -61,7 +62,7 @@ def run(args): workbook = get_workbook_by_name(server, args.workbook) schedule = get_schedule_by_name(server, args.schedule) - assign_workbook_to_schedule(server, workbook, schedule) + assign_to_schedule(server, workbook, schedule) def main(): diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 0afee6702..c180ccbb1 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,6 +1,7 @@ from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, PaginationItem, ScheduleItem +from ...type_helpers import item_type import logging import copy @@ -69,10 +70,21 @@ def create(self, schedule_item): @api(version="2.8") - def add_workbook(self, schedule_id, workbook_id): - url = "{0}/{1}/workbooks".format(self.siteurl, schedule_id) - print(url) - add_req = RequestFactory.Schedule.add_workbook_req(workbook_id) + def add_to_schedule(self, schedule_id, workbook_or_datasource): + type_ = item_type(workbook_or_datasource) + id_ = getattr(workbook_or_datasource, 'id', None) + req_factory = getattr(RequestFactory.Schedule, 'add_{}_req'.format(type_), None) + if id_ is None: + # TODO: Raise an error + return False + if req_factory is None: + # TODO: Raise an error + return False + + url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) + + add_req = req_factory(id_) server_response = self.put_request(url, add_req) - logger.info("Added workbook {} to schedule {}".format(workbook_id, schedule_id)) - print(server_response.content) + # TOOD: Assert that server_response is 2xx, otherwise, throw an error + logger.info("Added {0} {1} to schedule {2}".format(type_, id_, schedule_id)) + return True diff --git a/tableauserverclient/type_helpers.py b/tableauserverclient/type_helpers.py new file mode 100644 index 000000000..943e70210 --- /dev/null +++ b/tableauserverclient/type_helpers.py @@ -0,0 +1,2 @@ +def item_type(obj): + return type(obj).__name__.replace('Item', '').lower() From 128b319e72869568edafd4e4f6c2a92a73d63291 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Thu, 16 Nov 2017 11:12:18 -0800 Subject: [PATCH 4/8] Adding ability to use datasources --- samples/set_refresh_schedule.py | 16 ++++++++-- .../server/endpoint/endpoint.py | 31 ++++++++++++++++++- .../server/endpoint/exceptions.py | 4 +++ .../server/endpoint/schedules_endpoint.py | 15 +++++---- tableauserverclient/server/request_factory.py | 14 ++++++--- 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index 4f1c7278a..bf92d36a6 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -35,6 +35,13 @@ def get_workbook_by_name(server, name): 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 @@ -42,7 +49,7 @@ def get_schedule_by_name(server, name): def assign_to_schedule(server, workbook_or_datasource, schedule): - retval = server.schedules.add_to_schedule(schedule.id, workbook_or_datasource) + server.schedules.add_to_schedule(schedule.id, workbook_or_datasource) def run(args): @@ -59,10 +66,13 @@ def run(args): server = TSC.Server(args.server, use_server_version=True) with server.auth.sign_in(tableau_auth): - workbook = get_workbook_by_name(server, args.workbook) + 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, workbook, schedule) + assign_to_schedule(server, item, schedule) def main(): diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index cdc52e4ce..62200c52e 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 @@ -110,6 +110,35 @@ def wrapper(self, *args, **kwargs): return _decorator +def item_must_be_of_type(**params): + + params = {k: [item_class.__name__ for item_class in v] for k, v in params.items()} + + """Annotate that a parameter must by of a specific item type for an endpoint. + + Args: + Key/value pairs of the form `parameter`=`iterable of Item classes` + Raises: + ItemTypeNotAllowed + Returns: + None + """ + def _decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + params_to_check = set(params) & set(kwargs) + for p in params_to_check: + value = type(kwargs[p]).__name__ + if value not in params[p]: + error = "Item {0} is not of the required types. Must be {}".format( + p, ", ".join(params[p]) + ) + raise ItemTypeNotAllowed(error) + return func(self, *args, **kwargs) + return wrapper + return _decorator + + def parameter_added_in(**params): '''Annotate minimum versions for new parameters or request options on an endpoint. diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 48002de1b..76c2bcf0c 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 \ No newline at end of file diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index c180ccbb1..fd4c868ba 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,6 +1,6 @@ -from .endpoint import Endpoint, api +from .endpoint import Endpoint, api, item_must_be_of_type from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, PaginationItem, ScheduleItem +from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem from ...type_helpers import item_type import logging import copy @@ -70,16 +70,15 @@ def create(self, schedule_item): @api(version="2.8") + @item_must_be_of_type(workbook_or_datasource=[WorkbookItem, DatasourceItem]) def add_to_schedule(self, schedule_id, workbook_or_datasource): type_ = item_type(workbook_or_datasource) - id_ = getattr(workbook_or_datasource, 'id', None) + + # id will exist because item_must_be_of_type ensures this + id_ = workbook_or_datasource.id req_factory = getattr(RequestFactory.Schedule, 'add_{}_req'.format(type_), None) - if id_ is None: - # TODO: Raise an error - return False if req_factory is None: - # TODO: Raise an error - return False + raise RuntimeError("Unable to find request factory for {}".format(type_)) url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index ccb89b701..66530f72d 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -209,11 +209,11 @@ def update_req(self, schedule_item): single_interval_element.attrib[expression] = value return ET.tostring(xml_request) - def add_workbook_req(self, workbook_id): + def _add_to_req(self, id_, type_): """ - + @@ -221,11 +221,17 @@ def add_workbook_req(self, workbook_id): xml_request = ET.Element('tsRequest') task_element = ET.SubElement(xml_request, 'task') refresh = ET.SubElement(task_element, 'extractRefresh') - workbook = ET.SubElement(refresh, "workbook") - workbook.attrib['id'] = workbook_id + 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): From 65c78577f2016065653c2609f7c9a375a3ac91ad Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 12:18:40 -0800 Subject: [PATCH 5/8] fix pep8 failures --- samples/set_refresh_schedule.py | 4 ++-- tableauserverclient/server/endpoint/endpoint.py | 5 ++--- tableauserverclient/server/endpoint/exceptions.py | 2 +- tableauserverclient/server/endpoint/schedules_endpoint.py | 5 ----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index bf92d36a6..edb94f47e 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -50,7 +50,7 @@ def get_schedule_by_name(server, name): 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 @@ -82,4 +82,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 62200c52e..cedb5478f 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -111,9 +111,6 @@ def wrapper(self, *args, **kwargs): def item_must_be_of_type(**params): - - params = {k: [item_class.__name__ for item_class in v] for k, v in params.items()} - """Annotate that a parameter must by of a specific item type for an endpoint. Args: @@ -123,6 +120,8 @@ def item_must_be_of_type(**params): Returns: None """ + params = {k: [item_class.__name__ for item_class in v] for k, v in params.items()} + def _decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 76c2bcf0c..d77cdea3e 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -34,4 +34,4 @@ class EndpointUnavailableError(Exception): class ItemTypeNotAllowed(Exception): - pass \ No newline at end of file + pass diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index fd4c868ba..3407c70d7 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -17,7 +17,6 @@ def baseurl(self): 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") @@ -27,7 +26,6 @@ def get(self, req_options=None): all_schedule_items = ScheduleItem.from_response(server_response.content, self.parent_srv.namespace) return all_schedule_items, pagination_item - @api(version="2.3") def delete(self, schedule_id): if not schedule_id: @@ -37,7 +35,6 @@ def delete(self, schedule_id): self.delete_request(url) logger.info("Deleted single schedule (ID: {0})".format(schedule_id)) - @api(version="2.3") def update(self, schedule_item): if not schedule_item.id: @@ -54,7 +51,6 @@ def update(self, schedule_item): updated_schedule = copy.copy(schedule_item) return updated_schedule._parse_common_tags(server_response.content, self.parent_srv.namespace) - @api(version="2.3") def create(self, schedule_item): if schedule_item.interval_item is None: @@ -68,7 +64,6 @@ def create(self, schedule_item): logger.info("Created new schedule (ID: {})".format(new_schedule.id)) return new_schedule - @api(version="2.8") @item_must_be_of_type(workbook_or_datasource=[WorkbookItem, DatasourceItem]) def add_to_schedule(self, schedule_id, workbook_or_datasource): From 9191c2ab50c1462e7459e83ffd99c7c5629ca4f7 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 15 Jan 2018 15:24:09 -0800 Subject: [PATCH 6/8] Addressing Tyler's feedback about metaprogramming --- .../server/endpoint/endpoint.py | 28 ------------- .../server/endpoint/schedules_endpoint.py | 40 ++++++++++--------- test/test_schedule.py | 33 +++++++++++++++ 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index cedb5478f..052eb6f68 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -110,34 +110,6 @@ def wrapper(self, *args, **kwargs): return _decorator -def item_must_be_of_type(**params): - """Annotate that a parameter must by of a specific item type for an endpoint. - - Args: - Key/value pairs of the form `parameter`=`iterable of Item classes` - Raises: - ItemTypeNotAllowed - Returns: - None - """ - params = {k: [item_class.__name__ for item_class in v] for k, v in params.items()} - - def _decorator(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - params_to_check = set(params) & set(kwargs) - for p in params_to_check: - value = type(kwargs[p]).__name__ - if value not in params[p]: - error = "Item {0} is not of the required types. Must be {}".format( - p, ", ".join(params[p]) - ) - raise ItemTypeNotAllowed(error) - return func(self, *args, **kwargs) - return wrapper - return _decorator - - def parameter_added_in(**params): '''Annotate minimum versions for new parameters or request options on an endpoint. diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 3407c70d7..8c56b99b2 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,4 +1,4 @@ -from .endpoint import Endpoint, api, item_must_be_of_type +from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem from ...type_helpers import item_type @@ -65,20 +65,24 @@ def create(self, schedule_item): return new_schedule @api(version="2.8") - @item_must_be_of_type(workbook_or_datasource=[WorkbookItem, DatasourceItem]) - def add_to_schedule(self, schedule_id, workbook_or_datasource): - type_ = item_type(workbook_or_datasource) - - # id will exist because item_must_be_of_type ensures this - id_ = workbook_or_datasource.id - req_factory = getattr(RequestFactory.Schedule, 'add_{}_req'.format(type_), None) - if req_factory is None: - raise RuntimeError("Unable to find request factory for {}".format(type_)) - - url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) - - add_req = req_factory(id_) - server_response = self.put_request(url, add_req) - # TOOD: Assert that server_response is 2xx, otherwise, throw an error - logger.info("Added {0} {1} to schedule {2}".format(type_, id_, schedule_id)) - return True + 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_) + _ = self.put_request(url, add_req) + # TODO: Assert that server_response is 2xx, otherwise, throw an error + logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) + return True + + 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) + + return all(results) diff --git a/test/test_schedule.py b/test/test_schedule.py index 965e414a8..ecbd82b3e 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,33 @@ 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) + print(baseurl) + + 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.assert_(result, "Added properly") + + def test_add_datasource(self): + self.server.version = "2.8" + baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + print(baseurl) + + 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.assert_(result, "Added properly") From cbd78d8105a0f3a5b373405b4d6d64200143442f Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Tue, 16 Jan 2018 08:26:23 -0800 Subject: [PATCH 7/8] Address error handling --- .../server/endpoint/schedules_endpoint.py | 16 ++++++++++------ tableauserverclient/type_helpers.py | 2 -- test/test_schedule.py | 6 ++---- 3 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 tableauserverclient/type_helpers.py diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 8c56b99b2..6633d2863 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,11 +1,14 @@ from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem -from ...type_helpers import item_type 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): @@ -71,10 +74,12 @@ 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_) - _ = self.put_request(url, add_req) - # TODO: Assert that server_response is 2xx, otherwise, throw an error + 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 True + return OK items = [] @@ -84,5 +89,4 @@ def add_to(resource, type_, req_factory): items.append((datasource, "datasource", RequestFactory.Schedule.add_datasource_req)) results = (add_to(*x) for x in items) - - return all(results) + return filter(lambda x: not x.result, results) diff --git a/tableauserverclient/type_helpers.py b/tableauserverclient/type_helpers.py deleted file mode 100644 index 943e70210..000000000 --- a/tableauserverclient/type_helpers.py +++ /dev/null @@ -1,2 +0,0 @@ -def item_type(obj): - return type(obj).__name__.replace('Item', '').lower() diff --git a/test/test_schedule.py b/test/test_schedule.py index ecbd82b3e..59f6f8915 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -189,7 +189,6 @@ def test_update(self): def test_add_workbook(self): self.server.version = "2.8" baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) - print(baseurl) with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: workbook_response = f.read().decode("utf-8") @@ -199,12 +198,11 @@ def test_add_workbook(self): 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.assert_(result, "Added properly") + 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) - print(baseurl) with open(DATASOURCE_GET_BY_ID_XML, "rb") as f: datasource_response = f.read().decode("utf-8") @@ -214,4 +212,4 @@ def test_add_datasource(self): 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.assert_(result, "Added properly") + self.assertEquals(0, len(result), "Added properly") From 4d82dceb222299758f33fff71837f8b36244c512 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Tue, 16 Jan 2018 08:31:00 -0800 Subject: [PATCH 8/8] Fixing a py3 difference --- tableauserverclient/server/endpoint/schedules_endpoint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 6633d2863..2de488bdb 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -89,4 +89,5 @@ def add_to(resource, type_, req_factory): items.append((datasource, "datasource", RequestFactory.Schedule.add_datasource_req)) results = (add_to(*x) for x in items) - return filter(lambda x: not x.result, results) + # list() is needed for python 3.x compatibility + return list(filter(lambda x: not x.result, results))