Skip to content

Commit 8f0609d

Browse files
committed
feat: parse linked task xml
1 parent 7822be0 commit 8f0609d

4 files changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from typing import List, Optional
2+
from defusedxml.ElementTree import fromstring
3+
4+
from tableauserverclient.models.schedule_item import ScheduleItem
5+
from tableauserverclient.models.task_item import TaskItem
6+
7+
class LinkedTaskItem:
8+
def __init__(self) -> None:
9+
self.id: Optional[str] = None
10+
self.num_steps: Optional[int] = None
11+
self.schedule: Optional[ScheduleItem] = None
12+
13+
@classmethod
14+
def from_response(cls, resp: bytes, namespace) -> List["LinkedTaskItem"]:
15+
parsed_response = fromstring(resp)
16+
return [cls._parse_element(x, namespace) for x in parsed_response.findall(".//t:linkedTasks[@id]", namespaces=namespace)]
17+
18+
@classmethod
19+
def _parse_element(cls, xml, namespace) -> "LinkedTaskItem":
20+
task = cls()
21+
task.id = xml.get("id")
22+
task.num_steps = int(xml.get("numSteps"))
23+
task.schedule = ScheduleItem.from_element(xml, namespace)[0]
24+
return task
25+
26+
class LinkedTaskStepItem:
27+
def __init__(self) -> None:
28+
self.id: Optional[str] = None
29+
self.step_number: Optional[int] = None
30+
self.stop_downstream_on_failure: Optional[bool] = None
31+
self.task_details: List[LinkedTaskFlowRunItem] = []
32+
33+
@classmethod
34+
def from_task_xml(cls, xml, namespace) -> List["LinkedTaskStepItem"]:
35+
return [cls._parse_element(x, namespace) for x in xml.findall(".//t:linkedTaskSteps[@id]", namespace)]
36+
37+
@classmethod
38+
def _parse_element(cls, xml, namespace) -> "LinkedTaskStepItem":
39+
step = cls()
40+
step.id = xml.get("id")
41+
step.step_number = int(xml.get("stepNumber"))
42+
step.stop_downstream_on_failure = string_to_bool(xml.get("stopDownstreamTasksOnFailure"))
43+
step.task_details = LinkedTaskFlowRunItem._parse_element(xml, namespace)
44+
return step
45+
46+
class LinkedTaskFlowRunItem:
47+
def __init__(self) -> None:
48+
self.flow_run_id: Optional[str] = None
49+
self.flow_run_priority: Optional[int] = None
50+
self.flow_run_consecutive_failed_count: Optional[int] = None
51+
self.flow_run_task_type: Optional[str] = None
52+
self.flow_id: Optional[str] = None
53+
self.flow_name: Optional[str] = None
54+
55+
@classmethod
56+
def _parse_element(cls, xml, namespace) -> List["LinkedTaskFlowRunItem"]:
57+
all_tasks = []
58+
for flow_run in xml.findall(".//t:flowRun[@id]", namespace):
59+
task = cls()
60+
task.flow_run_id = flow_run.get("id")
61+
task.flow_run_priority = int(flow_run.get("priority"))
62+
task.flow_run_consecutive_failed_count = int(flow_run.get("consecutiveFailedCount"))
63+
task.flow_run_task_type = flow_run.get("type")
64+
flow = flow_run.find(".//t:flow[@id]", namespace)
65+
task.flow_id = flow.get("id")
66+
task.flow_name = flow.get("name")
67+
all_tasks.append(task)
68+
69+
return all_tasks
70+
71+
72+
73+
def string_to_bool(s: str) -> bool:
74+
return s.lower() == "true"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Optional
2+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
3+
from tableauserverclient.server.request_options import RequestOptions
4+
5+
class LinkedTasks(QuerysetEndpoint):
6+
def __init__(self, parent_srv):
7+
super().__init__(parent_srv)
8+
self._parent_srv = parent_srv
9+
10+
@property
11+
def baseurl(self):
12+
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/linked"
13+
14+
@api(version="3.15")
15+
def get(self, req_options: Optional[RequestOptions] = None):
16+
...
17+

test/assets/linked_tasks_get.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-3.5.xsd">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="5"/>
4+
<linkedTasks>
5+
<linkedTasks id="1b8211dc-51a8-45ce-a831-b5921708e03e"
6+
numSteps="1">
7+
<schedule id="be077332-d01d-481b-b2f3-917e463d4dca"
8+
name="schedule-name"
9+
state="Active"
10+
priority="50"
11+
createdAt="2024-07-24T00:27:55Z"
12+
updatedAt="2024-07-24T01:42:15Z"
13+
type="Flow"
14+
frequency="daily"
15+
nextRunAt="2024-07-24T03:30:00Z"/>
16+
<linkedTaskSteps>
17+
<linkedTaskSteps id="f554a4df-bb6f-4294-94ee-9a709ef9bda0"
18+
stepNumber="1"
19+
stopDownstreamTasksOnFailure="true">
20+
<task>
21+
<flowRun id="e3d1fc25-5644-4e32-af35-58dcbd1dbd73"
22+
priority="1"
23+
consecutiveFailedCount="3"
24+
type="runFlow">
25+
<flow id="ab1231eb-b8ca-461e-a131-83f3c2b6a673"
26+
name="flow-name" />
27+
</flowRun>
28+
</task>
29+
</linkedTaskSteps>
30+
</linkedTaskSteps>
31+
</linkedTasks>
32+
</linkedTasks>
33+
</tsResponse>

test/test_linked_tasks.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from pathlib import Path
2+
import unittest
3+
4+
from defusedxml.ElementTree import fromstring
5+
import pytest
6+
7+
import tableauserverclient as TSC
8+
from tableauserverclient.models.linked_tasks_item import LinkedTaskItem, LinkedTaskStepItem, LinkedTaskFlowRunItem
9+
10+
asset_dir = (Path(__file__).parent / "assets").resolve()
11+
12+
GET_LINKED_TASKS = asset_dir / "linked_tasks_get.xml"
13+
14+
class TestLinkedTasks(unittest.TestCase):
15+
16+
def setUp(self) -> None:
17+
self.server = TSC.Server("http://test", False)
18+
19+
# Fake signin
20+
self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67"
21+
self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM"
22+
23+
# self.baseurl = self.server.linked_tasks.baseurl
24+
25+
def test_parse_linked_task_flow_run(self):
26+
xml = fromstring(GET_LINKED_TASKS.read_bytes())
27+
task_runs = LinkedTaskFlowRunItem._parse_element(xml, self.server.namespace)
28+
self.assertEqual(1, len(task_runs))
29+
task = task_runs[0]
30+
self.assertEqual(task.flow_run_id, "e3d1fc25-5644-4e32-af35-58dcbd1dbd73")
31+
self.assertEqual(task.flow_run_priority, 1)
32+
self.assertEqual(task.flow_run_consecutive_failed_count, 3)
33+
self.assertEqual(task.flow_run_task_type, "runFlow")
34+
self.assertEqual(task.flow_id, "ab1231eb-b8ca-461e-a131-83f3c2b6a673")
35+
self.assertEqual(task.flow_name, "flow-name")
36+
37+
38+
def test_parse_linked_task_step(self):
39+
xml = fromstring(GET_LINKED_TASKS.read_bytes())
40+
steps = LinkedTaskStepItem.from_task_xml(xml, self.server.namespace)
41+
self.assertEqual(1, len(steps))
42+
step = steps[0]
43+
self.assertEqual(step.id, "f554a4df-bb6f-4294-94ee-9a709ef9bda0")
44+
self.assertTrue(step.stop_downstream_on_failure)
45+
self.assertEqual(step.step_number, 1)
46+
self.assertEqual(1, len(step.task_details))
47+
task = step.task_details[0]
48+
self.assertEqual(task.flow_run_id, "e3d1fc25-5644-4e32-af35-58dcbd1dbd73")
49+
self.assertEqual(task.flow_run_priority, 1)
50+
self.assertEqual(task.flow_run_consecutive_failed_count, 3)
51+
self.assertEqual(task.flow_run_task_type, "runFlow")
52+
self.assertEqual(task.flow_id, "ab1231eb-b8ca-461e-a131-83f3c2b6a673")
53+
self.assertEqual(task.flow_name, "flow-name")
54+
55+
def test_parse_linked_task(self):
56+
tasks = LinkedTaskItem.from_response(GET_LINKED_TASKS.read_bytes(), self.server.namespace)
57+
self.assertEqual(1, len(tasks))
58+
task = tasks[0]
59+
self.assertEqual(task.id, "1b8211dc-51a8-45ce-a831-b5921708e03e")
60+
self.assertEqual(task.num_steps, 1)
61+
self.assertEqual(task.schedule.id, "be077332-d01d-481b-b2f3-917e463d4dca")
62+

0 commit comments

Comments
 (0)