diff --git a/samples/create_extract_task.py b/samples/create_extract_task.py new file mode 100644 index 000000000..8408f67ee --- /dev/null +++ b/samples/create_extract_task.py @@ -0,0 +1,84 @@ +#### +# This script demonstrates how to create extract tasks in Tableau Cloud +# using the Tableau Server Client. +# +# To run the script, you must have installed Python 3.7 or later. +#### + + +import argparse +import logging + +from datetime import time + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description="Creates sample extract refresh task.") + # Common options; please keep those in sync across all samples + parser.add_argument("--server", "-s", help="server address") + parser.add_argument("--site", "-S", help="site name") + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="desired logging level (set to error by default)", + ) + # Options specific to this sample: + # This sample has no additional options, yet. If you add some, please add them here + + args = parser.parse_args() + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=False) + server.add_http_options({"verify": False}) + server.use_server_version() + with server.auth.sign_in(tableau_auth): + # Monthly Schedule + # This schedule will run on the 15th of every month at 11:30PM + monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) + monthly_schedule = TSC.ScheduleItem( + None, + None, + None, + None, + monthly_interval, + ) + + # Default to using first workbook found in server + all_workbook_items, pagination_item = server.workbooks.get() + my_workbook: TSC.WorkbookItem = all_workbook_items[0] + + target_item = TSC.Target( + my_workbook.id, # the id of the workbook or datasource + "workbook", # alternatively can be "datasource" + ) + + extract_item = TSC.TaskItem( + None, + "FullRefresh", + None, + None, + None, + monthly_schedule, + None, + target_item, + ) + + try: + response = server.tasks.create(extract_item) + print(response) + except Exception as e: + print(e) + + +if __name__ == "__main__": + main() diff --git a/tableauserverclient/models/subscription_item.py b/tableauserverclient/models/subscription_item.py index e18adc6ae..e96fcc448 100644 --- a/tableauserverclient/models/subscription_item.py +++ b/tableauserverclient/models/subscription_item.py @@ -4,6 +4,7 @@ from .property_decorators import property_is_boolean from .target import Target +from tableauserverclient.models import ScheduleItem if TYPE_CHECKING: from .target import Target @@ -23,6 +24,7 @@ def __init__(self, subject: str, schedule_id: str, user_id: str, target: "Target self.suspended = False self.target = target self.user_id = user_id + self.schedule = None def __repr__(self) -> str: if self.id is not None: @@ -92,9 +94,14 @@ def _parse_element(cls, element, ns): # Schedule element schedule_id = None + schedule = None if schedule_element is not None: schedule_id = schedule_element.get("id", None) + # If schedule id is not provided, then TOL with full schedule provided + if schedule_id is None: + schedule = ScheduleItem.from_element(element, ns) + # Content element target = None send_if_view_empty = None @@ -127,6 +134,7 @@ def _parse_element(cls, element, ns): sub.page_size_option = page_size_option sub.send_if_view_empty = send_if_view_empty sub.suspended = suspended + sub.schedule = schedule return sub diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py index ad1702f58..092597388 100644 --- a/tableauserverclient/server/endpoint/tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -51,6 +51,17 @@ def get_by_id(self, task_id): server_response = self.get_request(url) return TaskItem.from_response(server_response.content, self.parent_srv.namespace)[0] + @api(version="3.19") + def create(self, extract_item: TaskItem) -> TaskItem: + if not extract_item: + error = "No extract refresh provided" + raise ValueError(error) + logger.info("Creating an extract refresh ({})".format(extract_item)) + url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh)) + create_req = RequestFactory.Task.create_extract_req(extract_item) + server_response = self.post_request(url, create_req) + return server_response.content + @api(version="2.6") def run(self, task_item): if not task_item.id: diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 4bd30bb2c..7fb9bf9ed 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1028,6 +1028,34 @@ def run_req(self, xml_request, task_item): # Send an empty tsRequest pass + @_tsrequest_wrapped + def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes: + extract_element = ET.SubElement(xml_request, "extractRefresh") + + # Schedule attributes + schedule_element = ET.SubElement(xml_request, "schedule") + + interval_item = extract_item.schedule_item.interval_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 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") + for interval in interval_item._interval_type_pairs(): + expression, value = interval + single_interval_element = ET.SubElement(intervals_element, "interval") + single_interval_element.attrib[expression] = value + + # Main attributes + extract_element.attrib["type"] = extract_item.task_type + + target_element = ET.SubElement(extract_element, extract_item.target.type) + target_element.attrib["id"] = extract_item.target.id + + return ET.tostring(xml_request) + class SubscriptionRequest(object): @_tsrequest_wrapped diff --git a/test/assets/tasks_create_extract_task.xml b/test/assets/tasks_create_extract_task.xml new file mode 100644 index 000000000..9e6310fba --- /dev/null +++ b/test/assets/tasks_create_extract_task.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_task.py b/test/test_task.py index 5c432208d..4eb2c02e2 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -1,5 +1,6 @@ import os import unittest +from datetime import time import requests_mock @@ -15,12 +16,13 @@ GET_XML_WITH_WORKBOOK_AND_DATASOURCE = os.path.join(TEST_ASSET_DIR, "tasks_with_workbook_and_datasource.xml") GET_XML_DATAACCELERATION_TASK = os.path.join(TEST_ASSET_DIR, "tasks_with_dataacceleration_task.xml") GET_XML_RUN_NOW_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_run_now_response.xml") +GET_XML_CREATE_TASK_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_create_extract_task.xml") class TaskTests(unittest.TestCase): def setUp(self): self.server = TSC.Server("http://test", False) - self.server.version = "3.8" + self.server.version = "3.19" # Fake Signin self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" @@ -141,3 +143,26 @@ def test_run_now(self): self.assertTrue("7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" in job_response_content) self.assertTrue("RefreshExtract" in job_response_content) + + def test_create_extract_task(self): + monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) + monthly_schedule = TSC.ScheduleItem( + None, + None, + None, + None, + monthly_interval, + ) + target_item = TSC.Target("workbook_id", "workbook") + + task = TaskItem(None, "FullRefresh", None, schedule_item=monthly_schedule, target=target_item) + + with open(GET_XML_CREATE_TASK_RESPONSE, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post("{}".format(self.baseurl), text=response_xml) + create_response_content = self.server.tasks.create(task).decode("utf-8") + + self.assertTrue("task_id" in create_response_content) + self.assertTrue("workbook_id" in create_response_content) + self.assertTrue("FullRefresh" in create_response_content)