Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for custom schedules in TOL #1273

Merged
merged 3 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading