From 74af7490c39b9fb31dc2bf55efcf7cfb642cb86c Mon Sep 17 00:00:00 2001 From: dguo Date: Mon, 23 Dec 2019 16:56:00 -0800 Subject: [PATCH 1/6] fixing a bug due to bool(time(0,0)) is false --- tableauserverclient/server/request_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 90bda676c..d888d2575 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -276,7 +276,7 @@ def create_req(self, schedule_item): schedule_element.attrib['frequency'] = interval_item._frequency frequency_element = ET.SubElement(schedule_element, 'frequencyDetails') frequency_element.attrib['start'] = str(interval_item.start_time) - if hasattr(interval_item, 'end_time') and interval_item.end_time: + if hasattr(interval_item, 'end_time') and interval_item.end_time is not None: frequency_element.attrib['end'] = str(interval_item.end_time) if hasattr(interval_item, 'interval') and interval_item.interval: intervals_element = ET.SubElement(frequency_element, 'intervals') From eeb5260abf9144a0d6287ddb2e0554cd839b1826 Mon Sep 17 00:00:00 2001 From: dguo Date: Wed, 15 Jan 2020 15:52:18 -0800 Subject: [PATCH 2/6] check warnings in Schedules.add_to_schedule --- .../server/endpoint/schedules_endpoint.py | 18 +++++++++--- test/assets/schedule_add_datasource.xml | 9 ++++++ test/assets/schedule_add_workbook.xml | 9 ++++++ .../schedule_add_workbook_with_warnings.xml | 12 ++++++++ test/test_schedule.py | 28 +++++++++++++++++-- 5 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 test/assets/schedule_add_datasource.xml create mode 100644 test/assets/schedule_add_workbook.xml create mode 100644 test/assets/schedule_add_workbook_with_warnings.xml diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index ccba83565..5b15abc49 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,3 +1,4 @@ +import xml.etree.ElementTree as ET from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem, TaskItem @@ -7,8 +8,8 @@ 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) +AddResponse = namedtuple('AddResponse', ('result', 'error', 'warnings')) +OK = AddResponse(result=True, error=None, warnings=None) class Schedules(Endpoint): @@ -70,15 +71,24 @@ def create(self, schedule_item): @api(version="2.8") def add_to_schedule(self, schedule_id, workbook=None, datasource=None, task_type=TaskItem.Type.ExtractRefresh): + def read_warnings(response): + parsed_response = ET.fromstring(response) + all_warning_xml = parsed_response.findall('.//t:warning', namespaces=self.parent_srv.namespace) + warnings = list() if len(all_warning_xml) > 0 else None + for warning_xml in all_warning_xml: + warnings.append(warning_xml.get('message', None)) + return warnings 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_, task_type=task_type) response = self.put_request(url, add_req) - if response.status_code < 200 or response.status_code >= 300: + warnings = read_warnings(response.content) + if response.status_code < 200 or response.status_code >= 300 or warnings: return AddResponse(result=False, - error="Status {}: {}".format(response.status_code, response.reason)) + error="Status {}: {}".format(response.status_code, response.reason), + warnings=warnings) logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) return OK diff --git a/test/assets/schedule_add_datasource.xml b/test/assets/schedule_add_datasource.xml new file mode 100644 index 000000000..09ae94fcb --- /dev/null +++ b/test/assets/schedule_add_datasource.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/assets/schedule_add_workbook.xml b/test/assets/schedule_add_workbook.xml new file mode 100644 index 000000000..a6adb005e --- /dev/null +++ b/test/assets/schedule_add_workbook.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/assets/schedule_add_workbook_with_warnings.xml b/test/assets/schedule_add_workbook_with_warnings.xml new file mode 100644 index 000000000..0c376d018 --- /dev/null +++ b/test/assets/schedule_add_workbook_with_warnings.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_schedule.py b/test/test_schedule.py index 310a2b84a..8269cbb43 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -14,6 +14,9 @@ CREATE_WEEKLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_weekly.xml") CREATE_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_monthly.xml") UPDATE_XML = os.path.join(TEST_ASSET_DIR, "schedule_update.xml") +ADD_WORKBOOK_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook.xml") +ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook_with_warnings.xml") +ADD_DATASOURCE_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_datasource.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') @@ -208,24 +211,43 @@ def test_add_workbook(self): with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: workbook_response = f.read().decode("utf-8") + with open(ADD_WORKBOOK_TO_SCHEDULE, "rb") as f: + add_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") + m.put(baseurl + '/foo/workbooks', text=add_workbook_response) 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_workbook_with_warnings(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 open(ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS, "rb") as f: + add_workbook_response = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.get(self.server.workbooks.baseurl + '/bar', text=workbook_response) + m.put(baseurl + '/foo/workbooks', text=add_workbook_response) + workbook = self.server.workbooks.get_by_id("bar") + result = self.server.schedules.add_to_schedule('foo', workbook=workbook) + self.assertEqual(1, len(result), "Not added properly") + self.assertEqual(2, len(result[0].warnings)) + 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 open(ADD_DATASOURCE_TO_SCHEDULE, "rb") as f: + add_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") + m.put(baseurl + '/foo/datasources', text=add_datasource_response) 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") From af770ea71b2d2a207d2596446b9df9ea481ee94e Mon Sep 17 00:00:00 2001 From: dguo Date: Wed, 15 Jan 2020 15:55:17 -0800 Subject: [PATCH 3/6] fixed a typo --- test/assets/schedule_add_datasource.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/assets/schedule_add_datasource.xml b/test/assets/schedule_add_datasource.xml index 09ae94fcb..e57d2c8d2 100644 --- a/test/assets/schedule_add_datasource.xml +++ b/test/assets/schedule_add_datasource.xml @@ -3,7 +3,7 @@ - + From 3f570229a7e754fa2db675fa0cef9ff3fdc34a85 Mon Sep 17 00:00:00 2001 From: dguo Date: Wed, 15 Jan 2020 15:57:19 -0800 Subject: [PATCH 4/6] removed unused comment --- test/test_schedule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_schedule.py b/test/test_schedule.py index 8269cbb43..b7b047d02 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -214,7 +214,6 @@ def test_add_workbook(self): with open(ADD_WORKBOOK_TO_SCHEDULE, "rb") as f: add_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=add_workbook_response) workbook = self.server.workbooks.get_by_id("bar") From 184f9c994dd25d22eb95b186f3515be77048f762 Mon Sep 17 00:00:00 2001 From: dguo Date: Thu, 16 Jan 2020 17:36:14 -0800 Subject: [PATCH 5/6] handle the case where a task was indeed created, but there is a warning --- tableauserverclient/models/schedule_item.py | 24 ++++++++++++--- .../server/endpoint/schedules_endpoint.py | 29 ++++++++----------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index 94823d6c2..5ece2f8fe 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -162,10 +162,7 @@ def from_response(cls, resp, ns): @classmethod def from_element(cls, parsed_response, ns): - all_warning_xml = parsed_response.findall('.//t:warning', namespaces=ns) - warnings = list() if len(all_warning_xml) > 0 else None - for warning_xml in all_warning_xml: - warnings.append(warning_xml.get('message', None)) + warnings = cls._read_warnings(parsed_response, ns) all_schedule_items = [] all_schedule_xml = parsed_response.findall('.//t:schedule', namespaces=ns) @@ -248,3 +245,22 @@ def _parse_element(schedule_xml, ns): return id, name, state, created_at, updated_at, schedule_type, \ next_run_at, end_schedule_at, execution_order, priority, interval_item + + @staticmethod + def parse_add_to_schedule_response(response, ns): + parsed_response = ET.fromstring(response.content) + warnings = ScheduleItem._read_warnings(parsed_response, ns) + all_task_xml = parsed_response.findall('.//t:task', namespaces=ns) + + error = "Status {}: {}".format(response.status_code, response.reason) \ + if response.status_code < 200 or response.status_code >= 300 else None + task_created = len(all_task_xml) > 0 + return error, warnings, task_created + + @staticmethod + def _read_warnings(parsed_response, ns): + all_warning_xml = parsed_response.findall('.//t:warning', namespaces=ns) + warnings = list() if len(all_warning_xml) > 0 else None + for warning_xml in all_warning_xml: + warnings.append(warning_xml.get('message', None)) + return warnings diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 5b15abc49..09906c40e 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -8,8 +8,8 @@ logger = logging.getLogger('tableau.endpoint.schedules') # Oh to have a first class Result concept in Python... -AddResponse = namedtuple('AddResponse', ('result', 'error', 'warnings')) -OK = AddResponse(result=True, error=None, warnings=None) +AddResponse = namedtuple('AddResponse', ('result', 'error', 'warnings', 'task_created')) +OK = AddResponse(result=True, error=None, warnings=None, task_created=None) class Schedules(Endpoint): @@ -71,26 +71,21 @@ def create(self, schedule_item): @api(version="2.8") def add_to_schedule(self, schedule_id, workbook=None, datasource=None, task_type=TaskItem.Type.ExtractRefresh): - def read_warnings(response): - parsed_response = ET.fromstring(response) - all_warning_xml = parsed_response.findall('.//t:warning', namespaces=self.parent_srv.namespace) - warnings = list() if len(all_warning_xml) > 0 else None - for warning_xml in all_warning_xml: - warnings.append(warning_xml.get('message', None)) - return warnings - 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_, task_type=task_type) response = self.put_request(url, add_req) - warnings = read_warnings(response.content) - if response.status_code < 200 or response.status_code >= 300 or warnings: - return AddResponse(result=False, - error="Status {}: {}".format(response.status_code, response.reason), - warnings=warnings) - logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) - return OK + + error, warnings, task_created = ScheduleItem.parse_add_to_schedule_response( + response, self.parent_srv.namespace) + if task_created: + logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) + + if error is not None or warnings is not None: + return AddResponse(result=False, error=error, warnings=warnings, task_created=task_created) + else: + return OK items = [] From 71b9c442c69f834daebb53aac919bae03303a0fb Mon Sep 17 00:00:00 2001 From: dguo Date: Thu, 16 Jan 2020 17:41:22 -0800 Subject: [PATCH 6/6] remove unnecessary import --- tableauserverclient/server/endpoint/schedules_endpoint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 09906c40e..06fb7e408 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,4 +1,3 @@ -import xml.etree.ElementTree as ET from .endpoint import Endpoint, api from .exceptions import MissingRequiredFieldError from .. import RequestFactory, PaginationItem, ScheduleItem, WorkbookItem, DatasourceItem, TaskItem