Skip to content

Commit cb28446

Browse files
determine what datasources or workbooks are associated with a schedule
1 parent dbd0c0f commit cb28446

5 files changed

Lines changed: 142 additions & 1 deletion

File tree

tableauserverclient/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem
4949
from tableauserverclient.models.webhook_item import WebhookItem
5050
from tableauserverclient.models.workbook_item import WorkbookItem
51+
from tableauserverclient.models.extract_item import ExtractItem
5152

5253
__all__ = [
5354
"ColumnItem",
@@ -103,4 +104,5 @@
103104
"LinkedTaskItem",
104105
"LinkedTaskStepItem",
105106
"LinkedTaskFlowRunItem",
107+
"ExtractItem",
106108
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import Optional, List
2+
from defusedxml.ElementTree import fromstring
3+
import xml.etree.ElementTree as ET
4+
5+
6+
class ExtractItem:
7+
"""
8+
An extract refresh task item.
9+
10+
Attributes
11+
----------
12+
id : str
13+
The ID of the extract refresh task
14+
priority : int
15+
The priority of the task
16+
type : str
17+
The type of extract refresh (incremental or full)
18+
workbook_id : str, optional
19+
The ID of the workbook if this is a workbook extract
20+
datasource_id : str, optional
21+
The ID of the datasource if this is a datasource extract
22+
"""
23+
24+
def __init__(
25+
self,
26+
priority: int,
27+
refresh_type: str,
28+
workbook_id: Optional[str] = None,
29+
datasource_id: Optional[str] = None
30+
):
31+
self._id: Optional[str] = None
32+
self._priority = priority
33+
self._type = refresh_type
34+
self._workbook_id = workbook_id
35+
self._datasource_id = datasource_id
36+
37+
@property
38+
def id(self) -> Optional[str]:
39+
return self._id
40+
41+
@property
42+
def priority(self) -> int:
43+
return self._priority
44+
45+
@property
46+
def type(self) -> str:
47+
return self._type
48+
49+
@property
50+
def workbook_id(self) -> Optional[str]:
51+
return self._workbook_id
52+
53+
@property
54+
def datasource_id(self) -> Optional[str]:
55+
return self._datasource_id
56+
57+
@classmethod
58+
def from_response(cls, resp: str, ns: dict) -> List["ExtractItem"]:
59+
"""Create ExtractItem objects from XML response."""
60+
parsed_response = fromstring(resp)
61+
return cls.from_xml_element(parsed_response, ns)
62+
63+
@classmethod
64+
def from_xml_element(cls, parsed_response: ET.Element, ns: dict) -> List["ExtractItem"]:
65+
"""Create ExtractItem objects from XML element."""
66+
all_extract_items = []
67+
all_extract_xml = parsed_response.findall(".//t:extract", namespaces=ns)
68+
69+
for extract_xml in all_extract_xml:
70+
extract_id = extract_xml.get("id", None)
71+
priority = int(extract_xml.get("priority", 0))
72+
refresh_type = extract_xml.get("type", None)
73+
74+
# Check for workbook or datasource
75+
workbook_elem = extract_xml.find(".//t:workbook", namespaces=ns)
76+
datasource_elem = extract_xml.find(".//t:datasource", namespaces=ns)
77+
78+
workbook_id = workbook_elem.get("id", None) if workbook_elem is not None else None
79+
datasource_id = datasource_elem.get("id", None) if datasource_elem is not None else None
80+
81+
extract_item = cls(priority, refresh_type, workbook_id, datasource_id)
82+
extract_item._id = extract_id
83+
84+
all_extract_items.append(extract_item)
85+
86+
return all_extract_items

tableauserverclient/server/endpoint/schedules_endpoint.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .endpoint import Endpoint, api, parameter_added_in
88
from .exceptions import MissingRequiredFieldError
99
from tableauserverclient.server import RequestFactory
10-
from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem
10+
from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem, ExtractItem
1111

1212
from tableauserverclient.helpers.logging import logger
1313

@@ -149,3 +149,19 @@ def _add_to(
149149
)
150150
else:
151151
return OK
152+
153+
@api(version="2.3")
154+
def get_extract_refresh_tasks(self, schedule_id: str, req_options: Optional["RequestOptions"] = None) -> tuple[list["ExtractItem"], "PaginationItem"]:
155+
"""Get all extract refresh tasks for the specified schedule."""
156+
if not schedule_id:
157+
error = "Schedule ID undefined"
158+
raise ValueError(error)
159+
160+
logger.info(f"Querying extract refresh tasks for schedule (ID: {schedule_id})")
161+
url = f"{self.siteurl}/{schedule_id}/extracts"
162+
server_response = self.get_request(url, req_options)
163+
164+
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
165+
extract_items = ExtractItem.from_response(server_response.content, self.parent_srv.namespace)
166+
167+
return extract_items, pagination_item
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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-2.3.xsd">
3+
<extracts>
4+
<extract id="task1"
5+
priority="1"
6+
type="incremental-or-full" >
7+
<workbook id="workbook-id" />
8+
</extract>
9+
<extract id="task2"
10+
priority="2"
11+
type="incremental-or-full" >
12+
<datasource id="datasource-id" />
13+
</extract>
14+
</extracts>
15+
</tsResponse>

test/test_schedule.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook_with_warnings.xml")
2626
ADD_DATASOURCE_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_datasource.xml")
2727
ADD_FLOW_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_flow.xml")
28+
GET_EXTRACT_TASKS_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_extract_refresh_tasks.xml")
2829

2930
WORKBOOK_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id.xml")
3031
DATASOURCE_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "datasource_get_by_id.xml")
@@ -405,3 +406,24 @@ def test_add_flow(self) -> None:
405406
flow = self.server.flows.get_by_id("bar")
406407
result = self.server.schedules.add_to_schedule("foo", flow=flow)
407408
self.assertEqual(0, len(result), "Added properly")
409+
410+
def test_get_extract_refresh_tasks(self) -> None:
411+
self.server.version = "2.3"
412+
413+
414+
with open(GET_EXTRACT_TASKS_XML, "rb") as f:
415+
response_xml = f.read().decode("utf-8")
416+
with requests_mock.mock() as m:
417+
schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467"
418+
# baseurl = f"{self.baseurl}/schedules/{schedule_id}/extracts"
419+
baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules/{schedule_id}/extracts"
420+
# Fix the URL construction to match the endpoint pattern
421+
# url = f"{self.baseurl}/{schedule_id}/extracts"
422+
m.get(baseurl, text=response_xml)
423+
424+
extracts = self.server.schedules.get_extract_refresh_tasks(schedule_id)
425+
426+
self.assertIsNotNone(extracts)
427+
self.assertIsInstance(extracts[0], list)
428+
self.assertEqual(2, len(extracts[0]))
429+
self.assertEqual("task1", extracts[0][0].id)

0 commit comments

Comments
 (0)