Skip to content

Commit

Permalink
add support for custom schedules in TOL (tableau#1273)
Browse files Browse the repository at this point in the history
* add support for custom schedules in TOL
  • Loading branch information
a-torres-2 committed Aug 29, 2023
1 parent 01e0372 commit 5a5772c
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 1 deletion.
84 changes: 84 additions & 0 deletions samples/create_extract_task.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 8 additions & 0 deletions tableauserverclient/models/subscription_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions tableauserverclient/server/endpoint/tasks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions test/assets/tasks_create_extract_task.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_19.xsd">
<extractRefresh id="task_id" type="FullRefresh">
<workbook id="workbook_id"/>
</extractRefresh>
<schedule createdAt="2023-08-17T15:36:37-0700" updatedAt="2023-08-17T15:36:37-0700" frequency="Monthly" nextRunAt="2023-09-15T23:30:00-0700">
<frequencyDetails start="23:30:00">
<intervals>
<interval monthDay="15"/>
</intervals>
</frequencyDetails>
</schedule>
</tsResponse>
27 changes: 26 additions & 1 deletion test/test_task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import unittest
from datetime import time

import requests_mock

Expand All @@ -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"
Expand Down Expand Up @@ -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)

0 comments on commit 5a5772c

Please sign in to comment.