diff --git a/samples/extracts.py b/samples/extracts.py index 8e7a66aac..d9289452a 100644 --- a/samples/extracts.py +++ b/samples/extracts.py @@ -42,7 +42,6 @@ def main(): server.add_http_options({"verify": False}) server.use_server_version() with server.auth.sign_in(tableau_auth): - wb = None ds = None if args.workbook: diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 746bb24dd..30cd88104 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -49,6 +49,7 @@ from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem from tableauserverclient.models.webhook_item import WebhookItem from tableauserverclient.models.workbook_item import WorkbookItem +from tableauserverclient.models.extract_item import ExtractItem __all__ = [ "ColumnItem", @@ -106,4 +107,5 @@ "LinkedTaskItem", "LinkedTaskStepItem", "LinkedTaskFlowRunItem", + "ExtractItem", ] diff --git a/tableauserverclient/models/extract_item.py b/tableauserverclient/models/extract_item.py new file mode 100644 index 000000000..7562ffdde --- /dev/null +++ b/tableauserverclient/models/extract_item.py @@ -0,0 +1,82 @@ +from typing import Optional, List +from defusedxml.ElementTree import fromstring +import xml.etree.ElementTree as ET + + +class ExtractItem: + """ + An extract refresh task item. + + Attributes + ---------- + id : str + The ID of the extract refresh task + priority : int + The priority of the task + type : str + The type of extract refresh (incremental or full) + workbook_id : str, optional + The ID of the workbook if this is a workbook extract + datasource_id : str, optional + The ID of the datasource if this is a datasource extract + """ + + def __init__( + self, priority: int, refresh_type: str, workbook_id: Optional[str] = None, datasource_id: Optional[str] = None + ): + self._id: Optional[str] = None + self._priority = priority + self._type = refresh_type + self._workbook_id = workbook_id + self._datasource_id = datasource_id + + @property + def id(self) -> Optional[str]: + return self._id + + @property + def priority(self) -> int: + return self._priority + + @property + def type(self) -> str: + return self._type + + @property + def workbook_id(self) -> Optional[str]: + return self._workbook_id + + @property + def datasource_id(self) -> Optional[str]: + return self._datasource_id + + @classmethod + def from_response(cls, resp: str, ns: dict) -> List["ExtractItem"]: + """Create ExtractItem objects from XML response.""" + parsed_response = fromstring(resp) + return cls.from_xml_element(parsed_response, ns) + + @classmethod + def from_xml_element(cls, parsed_response: ET.Element, ns: dict) -> List["ExtractItem"]: + """Create ExtractItem objects from XML element.""" + all_extract_items = [] + all_extract_xml = parsed_response.findall(".//t:extract", namespaces=ns) + + for extract_xml in all_extract_xml: + extract_id = extract_xml.get("id", None) + priority = int(extract_xml.get("priority", 0)) + refresh_type = extract_xml.get("type", "") + + # Check for workbook or datasource + workbook_elem = extract_xml.find(".//t:workbook", namespaces=ns) + datasource_elem = extract_xml.find(".//t:datasource", namespaces=ns) + + workbook_id = workbook_elem.get("id", None) if workbook_elem is not None else None + datasource_id = datasource_elem.get("id", None) if datasource_elem is not None else None + + extract_item = cls(priority, refresh_type, workbook_id, datasource_id) + extract_item._id = extract_id + + all_extract_items.append(extract_item) + + return all_extract_items diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 8693d66cc..090d400b6 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -7,7 +7,7 @@ from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError from tableauserverclient.server import RequestFactory -from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem +from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem, ExtractItem from tableauserverclient.helpers.logging import logger @@ -261,3 +261,21 @@ def _add_to( ) else: return OK + + @api(version="2.3") + def get_extract_refresh_tasks( + self, schedule_id: str, req_options: Optional["RequestOptions"] = None + ) -> tuple[list["ExtractItem"], "PaginationItem"]: + """Get all extract refresh tasks for the specified schedule.""" + if not schedule_id: + error = "Schedule ID undefined" + raise ValueError(error) + + logger.info(f"Querying extract refresh tasks for schedule (ID: {schedule_id})") + url = f"{self.siteurl}/{schedule_id}/extracts" + server_response = self.get_request(url, req_options) + + pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) + extract_items = ExtractItem.from_response(server_response.content, self.parent_srv.namespace) + + return extract_items, pagination_item diff --git a/test/assets/schedule_get_extract_refresh_tasks.xml b/test/assets/schedule_get_extract_refresh_tasks.xml new file mode 100644 index 000000000..48906dde6 --- /dev/null +++ b/test/assets/schedule_get_extract_refresh_tasks.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/test/request_factory/test_task_requests.py b/test/request_factory/test_task_requests.py index 0258b8a93..6287fa6ea 100644 --- a/test/request_factory/test_task_requests.py +++ b/test/request_factory/test_task_requests.py @@ -5,7 +5,6 @@ class TestTaskRequest(unittest.TestCase): - def setUp(self): self.task_request = TaskRequest() self.xml_request = ET.Element("tsRequest") diff --git a/test/test_schedule.py b/test/test_schedule.py index b072522a4..4fcc85e18 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -25,6 +25,7 @@ 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") ADD_FLOW_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_flow.xml") +GET_EXTRACT_TASKS_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_extract_refresh_tasks.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") @@ -405,3 +406,20 @@ def test_add_flow(self) -> None: flow = self.server.flows.get_by_id("bar") result = self.server.schedules.add_to_schedule("foo", flow=flow) self.assertEqual(0, len(result), "Added properly") + + def test_get_extract_refresh_tasks(self) -> None: + self.server.version = "2.3" + + with open(GET_EXTRACT_TASKS_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules/{schedule_id}/extracts" + m.get(baseurl, text=response_xml) + + extracts = self.server.schedules.get_extract_refresh_tasks(schedule_id) + + self.assertIsNotNone(extracts) + self.assertIsInstance(extracts[0], list) + self.assertEqual(2, len(extracts[0])) + self.assertEqual("task1", extracts[0][0].id)