From 2552fbcbeee1083edfc3c1131b656e97a61d90e6 Mon Sep 17 00:00:00 2001 From: Renoy John Date: Fri, 21 Mar 2025 12:28:24 -0700 Subject: [PATCH 1/2] Adding incremental refresh option to workbook and datasource along with new job item finish code --- tableauserverclient/models/job_item.py | 1 + .../server/endpoint/datasources_endpoint.py | 3 +- .../server/endpoint/jobs_endpoint.py | 2 +- .../server/endpoint/workbooks_endpoint.py | 2 +- tableauserverclient/server/request_factory.py | 13 +++-- test/assets/job_get_by_id_completed.xml | 14 ++++++ test/request_factory/test_task_requests.py | 47 +++++++++++++++++++ test/test_job.py | 12 +++++ 8 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 test/assets/job_get_by_id_completed.xml create mode 100644 test/request_factory/test_task_requests.py diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 6286275c5..d650eb846 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -82,6 +82,7 @@ class FinishCode: Success: int = 0 Failed: int = 1 Cancelled: int = 2 + Completed: int = 3 def __init__( self, diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index e50a74ecb..69913a724 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -340,8 +340,7 @@ def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> """ id_ = getattr(datasource_item, "id", datasource_item) url = f"{self.baseurl}/{id_}/refresh" - # refresh_req = RequestFactory.Task.refresh_req(incremental) - refresh_req = RequestFactory.Empty.empty_req() + refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv) server_response = self.post_request(url, refresh_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] return new_job diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index 027a7ca12..48e91bd74 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -188,7 +188,7 @@ def wait_for_job(self, job_id: Union[str, JobItem], *, timeout: Optional[float] logger.info(f"Job {job_id} Completed: Finish Code: {job.finish_code} - Notes:{job.notes}") - if job.finish_code == JobItem.FinishCode.Success: + if job.finish_code in [JobItem.FinishCode.Success, JobItem.FinishCode.Completed]: return job elif job.finish_code == JobItem.FinishCode.Failed: raise JobFailedException(job) diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 8507152ba..bf4088b9f 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -140,7 +140,7 @@ def refresh(self, workbook_item: Union[WorkbookItem, str], incremental: bool = F """ id_ = getattr(workbook_item, "id", workbook_item) url = f"{self.baseurl}/{id_}/refresh" - refresh_req = RequestFactory.Task.refresh_req(incremental) + refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv) server_response = self.post_request(url, refresh_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] return new_job diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 79ac6e4ca..c24471e13 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1118,11 +1118,14 @@ def run_req(self, xml_request: ET.Element, task_item: Any) -> None: pass @_tsrequest_wrapped - def refresh_req(self, xml_request: ET.Element, incremental: bool = False) -> bytes: - task_element = ET.SubElement(xml_request, "extractRefresh") - if incremental: - task_element.attrib["incremental"] = "true" - return ET.tostring(xml_request) + def refresh_req(self, xml_request: ET.Element, incremental: bool = False, parent_srv: Optional["Server"] = None) -> bytes: + if parent_srv is not None and parent_srv.check_at_least_version("3.25"): + task_element = ET.SubElement(xml_request, "extractRefresh") + if incremental: + task_element.attrib["incremental"] = "true" + return ET.tostring(xml_request) + elif incremental: + raise ValueError("Incremental refresh is only supported in 3.25+") @_tsrequest_wrapped def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes: diff --git a/test/assets/job_get_by_id_completed.xml b/test/assets/job_get_by_id_completed.xml new file mode 100644 index 000000000..95ca29b49 --- /dev/null +++ b/test/assets/job_get_by_id_completed.xml @@ -0,0 +1,14 @@ + + + + + Job detail notes + + + More detail + + + \ No newline at end of file diff --git a/test/request_factory/test_task_requests.py b/test/request_factory/test_task_requests.py new file mode 100644 index 000000000..47354de23 --- /dev/null +++ b/test/request_factory/test_task_requests.py @@ -0,0 +1,47 @@ +import unittest +import xml.etree.ElementTree as ET +from unittest.mock import Mock +from tableauserverclient.server.request_factory import TaskRequest + +class TestTaskRequest(unittest.TestCase): + + def setUp(self): + self.task_request = TaskRequest() + self.xml_request = ET.Element("tsRequest") + + def test_refresh_req_default(self): + result = self.task_request.refresh_req() + self.assertEqual(result, ET.tostring(self.xml_request)) + + def test_refresh_req_incremental(self): + with self.assertRaises(ValueError): + self.task_request.refresh_req(incremental=True) + + def test_refresh_req_with_parent_srv_version_3_25(self): + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = True + result = self.task_request.refresh_req(incremental=True, parent_srv=parent_srv) + expected_xml = ET.Element("tsRequest") + task_element = ET.SubElement(expected_xml, "extractRefresh") + task_element.attrib["incremental"] = "true" + self.assertEqual(result, ET.tostring(expected_xml)) + + def test_refresh_req_with_parent_srv_version_3_25_non_incremental(self): + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = True + result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv) + expected_xml = ET.Element("tsRequest") + ET.SubElement(expected_xml, "extractRefresh") + self.assertEqual(result, ET.tostring(expected_xml)) + + def test_refresh_req_with_parent_srv_version_below_3_25(self): + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = False + with self.assertRaises(ValueError): + self.task_request.refresh_req(incremental=True, parent_srv=parent_srv) + + def test_refresh_req_with_parent_srv_version_below_3_25_non_incremental(self): + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = False + result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv) + self.assertEqual(result, ET.tostring(self.xml_request)) diff --git a/test/test_job.py b/test/test_job.py index 20b238764..b3d7007aa 100644 --- a/test/test_job.py +++ b/test/test_job.py @@ -11,6 +11,7 @@ GET_XML = "job_get.xml" GET_BY_ID_XML = "job_get_by_id.xml" +GET_BY_ID_COMPLETED_XML = "job_get_by_id_completed.xml" GET_BY_ID_FAILED_XML = "job_get_by_id_failed.xml" GET_BY_ID_CANCELLED_XML = "job_get_by_id_cancelled.xml" GET_BY_ID_INPROGRESS_XML = "job_get_by_id_inprogress.xml" @@ -87,6 +88,17 @@ def test_wait_for_job_finished(self) -> None: self.assertEqual(job_id, job.id) self.assertListEqual(job.notes, ["Job detail notes"]) + def test_wait_for_job_completed(self) -> None: + # Waiting for a bridge (cloud) job completion + response_xml = read_xml_asset(GET_BY_ID_COMPLETED_XML) + job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{self.baseurl}/{job_id}", text=response_xml) + job = self.server.jobs.wait_for_job(job_id) + + self.assertEqual(job_id, job.id) + self.assertListEqual(job.notes, ["Job detail notes"]) + def test_wait_for_job_failed(self) -> None: # Waiting for a failed job raises an exception response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) From a84ebb27dc5557b566269dc018f6d7f3799dcefc Mon Sep 17 00:00:00 2001 From: Renoy John Date: Fri, 21 Mar 2025 13:06:28 -0700 Subject: [PATCH 2/2] Fix build pipeline failures related to mypy & black --- tableauserverclient/server/request_factory.py | 5 ++++- test/request_factory/test_task_requests.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index c24471e13..575423612 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1118,7 +1118,9 @@ def run_req(self, xml_request: ET.Element, task_item: Any) -> None: pass @_tsrequest_wrapped - def refresh_req(self, xml_request: ET.Element, incremental: bool = False, parent_srv: Optional["Server"] = None) -> bytes: + def refresh_req( + self, xml_request: ET.Element, incremental: bool = False, parent_srv: Optional["Server"] = None + ) -> Optional[bytes]: if parent_srv is not None and parent_srv.check_at_least_version("3.25"): task_element = ET.SubElement(xml_request, "extractRefresh") if incremental: @@ -1126,6 +1128,7 @@ def refresh_req(self, xml_request: ET.Element, incremental: bool = False, parent return ET.tostring(xml_request) elif incremental: raise ValueError("Incremental refresh is only supported in 3.25+") + return None @_tsrequest_wrapped def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes: diff --git a/test/request_factory/test_task_requests.py b/test/request_factory/test_task_requests.py index 47354de23..0258b8a93 100644 --- a/test/request_factory/test_task_requests.py +++ b/test/request_factory/test_task_requests.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from tableauserverclient.server.request_factory import TaskRequest + class TestTaskRequest(unittest.TestCase): def setUp(self):