Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 0 additions & 1 deletion samples/extracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def main():
server.add_http_options({"verify": False})
server.use_server_version()
with server.auth.sign_in(tableau_auth):

wb = None
ds = None
if args.workbook:
Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from tableauserverclient.models.virtual_connection_item import VirtualConnectionItem
from tableauserverclient.models.webhook_item import WebhookItem
from tableauserverclient.models.workbook_item import WorkbookItem
from tableauserverclient.models.extract_item import ExtractItem

__all__ = [
"ColumnItem",
Expand Down Expand Up @@ -103,4 +104,5 @@
"LinkedTaskItem",
"LinkedTaskStepItem",
"LinkedTaskFlowRunItem",
"ExtractItem",
]
82 changes: 82 additions & 0 deletions tableauserverclient/models/extract_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from typing import Optional, List
from defusedxml.ElementTree import fromstring
import xml.etree.ElementTree as ET


class ExtractItem:
"""
An extract refresh task item.

Attributes
----------
id : str
The ID of the extract refresh task
priority : int
The priority of the task
type : str
The type of extract refresh (incremental or full)
workbook_id : str, optional
The ID of the workbook if this is a workbook extract
datasource_id : str, optional
The ID of the datasource if this is a datasource extract
"""

def __init__(
self, priority: int, refresh_type: str, workbook_id: Optional[str] = None, datasource_id: Optional[str] = None
):
self._id: Optional[str] = None
self._priority = priority
self._type = refresh_type
self._workbook_id = workbook_id
self._datasource_id = datasource_id

@property
def id(self) -> Optional[str]:
return self._id

@property
def priority(self) -> int:
return self._priority

@property
def type(self) -> str:
return self._type

@property
def workbook_id(self) -> Optional[str]:
return self._workbook_id

@property
def datasource_id(self) -> Optional[str]:
return self._datasource_id

@classmethod
def from_response(cls, resp: str, ns: dict) -> List["ExtractItem"]:
"""Create ExtractItem objects from XML response."""
parsed_response = fromstring(resp)
return cls.from_xml_element(parsed_response, ns)

@classmethod
def from_xml_element(cls, parsed_response: ET.Element, ns: dict) -> List["ExtractItem"]:
"""Create ExtractItem objects from XML element."""
all_extract_items = []
all_extract_xml = parsed_response.findall(".//t:extract", namespaces=ns)

for extract_xml in all_extract_xml:
extract_id = extract_xml.get("id", None)
priority = int(extract_xml.get("priority", 0))
refresh_type = extract_xml.get("type", "")

# Check for workbook or datasource
workbook_elem = extract_xml.find(".//t:workbook", namespaces=ns)
datasource_elem = extract_xml.find(".//t:datasource", namespaces=ns)

workbook_id = workbook_elem.get("id", None) if workbook_elem is not None else None
datasource_id = datasource_elem.get("id", None) if datasource_elem is not None else None

extract_item = cls(priority, refresh_type, workbook_id, datasource_id)
extract_item._id = extract_id

all_extract_items.append(extract_item)

return all_extract_items
12 changes: 8 additions & 4 deletions tableauserverclient/server/endpoint/groups_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,12 @@ def delete(self, group_id: str) -> None:
logger.info(f"Deleted single group (ID: {group_id})")

@overload
def update(self, group_item: GroupItem, as_job: Literal[False]) -> GroupItem: ...
def update(self, group_item: GroupItem, as_job: Literal[False]) -> GroupItem:
...

@overload
def update(self, group_item: GroupItem, as_job: Literal[True]) -> JobItem: ...
def update(self, group_item: GroupItem, as_job: Literal[True]) -> JobItem:
...

@api(version="2.0")
def update(self, group_item, as_job=False):
Expand Down Expand Up @@ -258,10 +260,12 @@ def create(self, group_item: GroupItem) -> GroupItem:
return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]

@overload
def create_AD_group(self, group_item: GroupItem, asJob: Literal[False]) -> GroupItem: ...
def create_AD_group(self, group_item: GroupItem, asJob: Literal[False]) -> GroupItem:
...

@overload
def create_AD_group(self, group_item: GroupItem, asJob: Literal[True]) -> JobItem: ...
def create_AD_group(self, group_item: GroupItem, asJob: Literal[True]) -> JobItem:
...

@api(version="2.0")
def create_AD_group(self, group_item, asJob=False):
Expand Down
20 changes: 19 additions & 1 deletion tableauserverclient/server/endpoint/schedules_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .endpoint import Endpoint, api, parameter_added_in
from .exceptions import MissingRequiredFieldError
from tableauserverclient.server import RequestFactory
from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem
from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem, ExtractItem

from tableauserverclient.helpers.logging import logger

Expand Down Expand Up @@ -261,3 +261,21 @@ def _add_to(
)
else:
return OK

@api(version="2.3")
def get_extract_refresh_tasks(
self, schedule_id: str, req_options: Optional["RequestOptions"] = None
) -> tuple[list["ExtractItem"], "PaginationItem"]:
"""Get all extract refresh tasks for the specified schedule."""
if not schedule_id:
error = "Schedule ID undefined"
raise ValueError(error)

logger.info(f"Querying extract refresh tasks for schedule (ID: {schedule_id})")
url = f"{self.siteurl}/{schedule_id}/extracts"
server_response = self.get_request(url, req_options)

pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
extract_items = ExtractItem.from_response(server_response.content, self.parent_srv.namespace)

return extract_items, pagination_item
6 changes: 4 additions & 2 deletions tableauserverclient/server/pager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@

@runtime_checkable
class Endpoint(Protocol[T]):
def get(self, req_options: Optional[RequestOptions]) -> tuple[list[T], PaginationItem]: ...
def get(self, req_options: Optional[RequestOptions]) -> tuple[list[T], PaginationItem]:
...


@runtime_checkable
class CallableEndpoint(Protocol[T]):
def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> tuple[list[T], PaginationItem]: ...
def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> tuple[list[T], PaginationItem]:
...


class Pager(Iterable[T]):
Expand Down
6 changes: 4 additions & 2 deletions tableauserverclient/server/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ def __iter__(self: Self) -> Iterator[T]:
return

@overload
def __getitem__(self: Self, k: Slice) -> list[T]: ...
def __getitem__(self: Self, k: Slice) -> list[T]:
...

@overload
def __getitem__(self: Self, k: int) -> T: ...
def __getitem__(self: Self, k: int) -> T:
...

def __getitem__(self, k):
page = self.page_number
Expand Down
6 changes: 3 additions & 3 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,9 +1008,9 @@ def update_req(self, workbook_item, parent_srv: Optional["Server"] = None):
if data_freshness_policy_config.option == "FreshEvery":
if data_freshness_policy_config.fresh_every_schedule is not None:
fresh_every_element = ET.SubElement(data_freshness_policy_element, "freshEverySchedule")
fresh_every_element.attrib["frequency"] = (
data_freshness_policy_config.fresh_every_schedule.frequency
)
fresh_every_element.attrib[
"frequency"
] = data_freshness_policy_config.fresh_every_schedule.frequency
fresh_every_element.attrib["value"] = str(data_freshness_policy_config.fresh_every_schedule.value)
else:
raise ValueError(f"data_freshness_policy_config.fresh_every_schedule must be populated.")
Expand Down
15 changes: 15 additions & 0 deletions test/assets/schedule_get_extract_refresh_tasks.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='UTF-8'?>
<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">
<extracts>
<extract id="task1"
priority="1"
type="incremental-or-full" >
<workbook id="workbook-id" />
</extract>
<extract id="task2"
priority="2"
type="incremental-or-full" >
<datasource id="datasource-id" />
</extract>
</extracts>
</tsResponse>
1 change: 0 additions & 1 deletion test/request_factory/test_task_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


class TestTaskRequest(unittest.TestCase):

def setUp(self):
self.task_request = TaskRequest()
self.xml_request = ET.Element("tsRequest")
Expand Down
21 changes: 21 additions & 0 deletions test/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook_with_warnings.xml")
ADD_DATASOURCE_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_datasource.xml")
ADD_FLOW_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_flow.xml")
GET_EXTRACT_TASKS_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_extract_refresh_tasks.xml")

WORKBOOK_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id.xml")
DATASOURCE_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "datasource_get_by_id.xml")
Expand Down Expand Up @@ -405,3 +406,23 @@ def test_add_flow(self) -> None:
flow = self.server.flows.get_by_id("bar")
result = self.server.schedules.add_to_schedule("foo", flow=flow)
self.assertEqual(0, len(result), "Added properly")

def test_get_extract_refresh_tasks(self) -> None:
self.server.version = "2.3"

with open(GET_EXTRACT_TASKS_XML, "rb") as f:
response_xml = f.read().decode("utf-8")
with requests_mock.mock() as m:
schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467"
# baseurl = f"{self.baseurl}/schedules/{schedule_id}/extracts"
baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules/{schedule_id}/extracts"
# Fix the URL construction to match the endpoint pattern
# url = f"{self.baseurl}/{schedule_id}/extracts"
m.get(baseurl, text=response_xml)

extracts = self.server.schedules.get_extract_refresh_tasks(schedule_id)

self.assertIsNotNone(extracts)
self.assertIsInstance(extracts[0], list)
self.assertEqual(2, len(extracts[0]))
self.assertEqual("task1", extracts[0][0].id)
Loading