8000 Merge pull request #1297 from jorwoods/jorwoods/tasks_no_schedule · daxcurson/server-client-python@696f20d · GitHub
[go: up one dir, main page]

Skip to content

Commit 696f20d

Browse files
authored
Merge pull request tableau#1297 from jorwoods/jorwoods/tasks_no_schedule
Jorwoods/tasks no schedule
2 parents e72552d + 11656c4 commit 696f20d

File tree

5 files changed

+71
-36
lines changed

5 files changed

+71
-36
lines changed

tableauserverclient/models/task_item.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from datetime import datetime
2+
from typing import List, Optional
3+
14
from defusedxml.ElementTree import fromstring
25

36
from tableauserverclient.datetime_helpers import parse_datetime
4-
from .schedule_item import ScheduleItem
5-
from .target import Target
7+
from tableauserverclient.models.schedule_item import ScheduleItem
8+
from tableauserverclient.models.target import Target
69

710

811
class TaskItem(object):
@@ -19,14 +22,14 @@ class Type:
1922

2023
def __init__(
2124
self,
22-
id_,
23-
task_type,
24-
priority,
25-
consecutive_failed_count=0,
26-
schedule_id=None,
27-
schedule_item=None,
28-
last_run_at=None,
29-
target=None,
25+
id_: str,
26+
task_type: str,
27+
priority: int,
28+
consecutive_failed_count: int = 0,
29+
schedule_id: Optional[str] = None,
30+
schedule_item: Optional[ScheduleItem] = None,
31+
last_run_at: Optional[datetime] = None,
32+
target: Optional[Target] = None,
3033
):
3134
self.id = id_
3235
self.task_type = task_type
@@ -37,14 +40,14 @@ def __init__(
3740
self.last_run_at = last_run_at
3841
self.target = target
3942

40-
def __repr__(self):
43+
def __repr__(self) -> str:
4144
return (
4245
"<Task#{id} {task_type} pri({priority}) failed({consecutive_failed_count}) schedule_id({"
4346
"schedule_id}) target({target})>".format(**self.__dict__)
4447
)
4548

4649
@classmethod
47-
def from_response(cls, xml, ns, task_type=Type.ExtractRefresh):
50+
def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) -> List["TaskItem"]:
4851
parsed_response = fromstring(xml)
4952
all_tasks_xml = parsed_response.findall(".//t:task/t:{}".format(task_type), namespaces=ns)
5053

@@ -62,8 +65,7 @@ def _parse_element(cls, element, ns):
6265
last_run_at_element = element.find(".//t:lastRunAt", namespaces=ns)
6366

6467
schedule_item_list = ScheduleItem.from_element(element, ns)
65-
if len(schedule_item_list) >= 1:
66-
schedule_item = schedule_item_list[0]
68+
schedule_item = next(iter(schedule_item_list), None)
6769

6870
# according to the Tableau Server REST API documentation,
6971
# there should be only one of workbook or datasource
@@ -87,14 +89,14 @@ def _parse_element(cls, element, ns):
8789
task_type,
8890
priority,
8991
consecutive_failed_count,
90-
schedule_item.id,
92+
schedule_item.id if schedule_item is not None else None,
9193
schedule_item,
9294
last_run_at,
9395
target,
9496
)
9597

9698
@staticmethod
97-
def _translate_task_type(task_type):
99+
def _translate_task_type(task_type: str) -> str:
98100
if task_type in TaskItem._TASK_TYPE_MAPPING:
99101
return TaskItem._TASK_TYPE_MAPPING[task_type]
100102
else:

tableauserverclient/server/endpoint/tasks_endpoint.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import logging
2+
from typing import List, Optional, Tuple, TYPE_CHECKING
23

3-
from .endpoint import Endpoint, api
4-
from .exceptions import MissingRequiredFieldError
4+
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
5+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
56
from tableauserverclient.models import TaskItem, PaginationItem
67
from tableauserverclient.server import RequestFactory
78

89
from tableauserverclient.helpers.logging import logger
910

11+
if TYPE_CHECKING:
12+
from tableauserverclient.server.request_options import RequestOptions
13+
1014

1115
class Tasks(Endpoint):
1216
@property
13-
def baseurl(self):
17+
def baseurl(self) -> str:
1418
return "{0}/sites/{1}/tasks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
1519

16-
def __normalize_task_type(self, task_type):
20+
def __normalize_task_type(self, task_type: str) -> str:
1721
"""
1822
The word for extract refresh used in API URL is "extractRefreshes".
1923
It is different than the tag "extractRefresh" used in the request body.
@@ -24,11 +28,13 @@ def __normalize_task_type(self, task_type):
2428
return task_type
2529

2630
@api(version="2.6")
27-
def get(self, req_options=None, task_type=TaskItem.Type.ExtractRefresh):
31+
def get(
32+
self, req_options: Optional["RequestOptions"] = None, task_type: str = TaskItem.Type.ExtractRefresh
33+
) -> Tuple[List[TaskItem], PaginationItem]:
2834
if task_type == TaskItem.Type.DataAcceleration:
2935
self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks")
3036

31-
logger.info("Querying all {} tasks for the site".format(task_type))
37+
logger.info("Querying all %s tasks for the site", task_type)
3238

3339
url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(task_type))
3440
server_response = self.get_request(url, req_options)
@@ -38,11 +44,11 @@ def get(self, req_options=None, task_type=TaskItem.Type.ExtractRefresh):
3844
return all_tasks, pagination_item
3945

4046
@api(version="2.6")
41-
def get_by_id(self, task_id):
47+
def get_by_id(self, task_id: str) -> TaskItem:
4248
if not task_id:
4349
error = "No Task ID provided"
4450
raise ValueError(error)
45-
logger.info("Querying a single task by id ({})".format(task_id))
51+
logger.info("Querying a single task by id %s", task_id)
4652
url = "{}/{}/{}".format(
4753
self.baseurl,
4854
self.__normalize_task_type(TaskItem.Type.ExtractRefresh),
@@ -56,14 +62,14 @@ def create(self, extract_item: TaskItem) -> TaskItem:
5662
if not extract_item:
5763
error = "No extract refresh provided"
5864
raise ValueError(error)
59-
logger.info("Creating an extract refresh ({})".format(extract_item))
65+
logger.info("Creating an extract refresh %s", extract_item)
6066
url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh))
6167
create_req = RequestFactory.Task.create_extract_req(extract_item)
6268
server_response = self.post_request(url, create_req)
6369
return server_response.content
6470

6571
@api(version="2.6")
66-
def run(self, task_item):
72+
def run(self, task_item: TaskItem) -> bytes:
6773
if not task_item.id:
6874
error = "Task item missing ID."
6975
raise MissingRequiredFieldError(error)
@@ -79,7 +85,7 @@ def run(self, task_item):
7985

8086
# Delete 1 task by id
8187
@api(version="3.6")
82-
def delete(self, task_id, task_type=TaskItem.Type.ExtractRefresh):
88+
def delete(self, task_id: str, task_type: str = TaskItem.Type.ExtractRefresh) -> None:
8389
if task_type == TaskItem.Type.DataAcceleration:
8490
self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks")
8591

@@ -88,4 +94,4 @@ def delete(self, task_id, task_type=TaskItem.Type.ExtractRefresh):
8894
raise ValueError(error)
8995
url = "{0}/{1}/{2}".format(self.baseurl, self.__normalize_task_type(task_type), task_id)
9096
self.delete_request(url)
91-
logger.info("Deleted single task (ID: {0})".format(task_id))
97+
logger.info("Deleted single task (ID: %s)", task_id)

tableauserverclient/server/request_factory.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,16 @@ def run_req(self, xml_request, task_item):
10321032
def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes:
10331033
extract_element = ET.SubElement(xml_request, "extractRefresh")
10341034

1035+
# Main attributes
1036+
extract_element.attrib["type"] = extract_item.task_type
1037+
1038+
if extract_item.target is not None:
1039+
target_element = ET.SubElement(extract_element, extract_item.target.type)
1040+
target_element.attrib["id"] = extract_item.target.id
1041+
1042+
if extract_item.schedule_item is None:
1043+
return ET.tostring(xml_request)
1044+
10351045
# Schedule attributes
10361046
schedule_element = ET.SubElement(xml_request, "schedule")
10371047

@@ -1043,17 +1053,11 @@ def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem")
10431053
frequency_element.attrib["end"] = str(interval_item.end_time)
10441054
if hasattr(interval_item, "interval") and interval_item.interval:
10451055
intervals_element = ET.SubElement(frequency_element, "intervals")
1046-
for interval in interval_item._interval_type_pairs():
1056+
for interval in interval_item._interval_type_pairs(): # type: ignore
10471057
expression, value = interval
10481058
single_interval_element = ET.SubElement(intervals_element, "interval")
10491059
single_interval_element.attrib[expression] = value
10501060

1051-
# Main attributes
1052-
extract_element.attrib["type"] = extract_item.task_type
1053-
1054-
target_element = ET.SubElement(extract_element, extract_item.target.type)
1055-
target_element.attrib["id"] = extract_item.target.id
1056-
10571061
return ET.tostring(xml_request)
10581062

10591063

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse
3+
xmlns="http://tableau.com/api"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.6.xsd">
5+
<tasks>
6+
<task>
7+
<extractRefresh id="f84901ac-72ad-4f9b-a87e-7a3500402ad6" priority="50" consecutiveFailedCount="0" type="RefreshExtractTask">
8+
<datasource id="c7a9327e-1cda-4504-b026-ddb43b976d1d" />
9+
</extractRefresh>
10+
</task>
11+
</tasks>
12+
</tsResponse>

test/test_task.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import os
22
import unittest
33
from datetime import time
4+
from pathlib import Path
45

56
import requests_mock
67

78
import tableauserverclient as TSC
89
from tableauserverclient.datetime_helpers import parse_datetime
910
from tableauserverclient.models.task_item import TaskItem
1011

11-
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
12+
TEST_ASSET_DIR = Path(__file__).parent / "assets"
1213

1314
GET_XML_NO_WORKBOOK = os.path.join(TEST_ASSET_DIR, "tasks_no_workbook_or_datasource.xml")
1415
GET_XML_WITH_WORKBOOK = os.path.join(TEST_ASSET_DIR, "tasks_with_workbook.xml")
@@ -17,6 +18,7 @@
1718
GET_XML_DATAACCELERATION_TASK = os.path.join(TEST_ASSET_DIR, "tasks_with_dataacceleration_task.xml")
1819
GET_XML_RUN_NOW_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_run_now_response.xml")
1920
GET_XML_CREATE_TASK_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_create_extract_task.xml")
21+
GET_XML_WITHOUT_SCHEDULE = TEST_ASSET_DIR / "tasks_without_schedule.xml"
2022

2123

2224
class TaskTests(unittest.TestCase):
@@ -86,6 +88,15 @@ def test_get_task_with_schedule(self):
8688
self.assertEqual("workbook", task.target.type)
8789
self.assertEqual("b60b4efd-a6f7-4599-beb3-cb677e7abac1", task.schedule_id)
8890

91+
def test_get_task_without_schedule(self):
92+
with requests_mock.mock() as m:
93+
m.get(self.baseurl, text=GET_XML_WITHOUT_SCHEDULE.read_text())
94+
all_tasks, pagination_item = self.server.tasks.get()
95+
96+
task = all_tasks[0]
97+
self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id)
98+
self.assertEqual("datasource", task.target.type)
99+
89100
def test_delete(self):
90101
with requests_mock.mock() as m:
91102
m.delete(self.baseurl + "/c7a9327e-1cda-4504-b026-ddb43b976d1d", status_code=204)

0 commit comments

Comments
 (0)
0