Skip to content

Commit 57b89cd

Browse files
committed
feat: batch update schedule state
1 parent cfeb23b commit 57b89cd

File tree

5 files changed

+132
-3
lines changed

5 files changed

+132
-3
lines changed

tableauserverclient/models/schedule_item.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import xml.etree.ElementTree as ET
22
from datetime import datetime
3-
from typing import Optional, Union
3+
from typing import Optional, Union, TYPE_CHECKING
44

55
from defusedxml.ElementTree import fromstring
66

@@ -16,6 +16,10 @@
1616
property_is_enum,
1717
)
1818

19+
if TYPE_CHECKING:
20+
from requests import Response
21+
22+
1923
Interval = Union[HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval]
2024

2125

@@ -407,3 +411,8 @@ def _read_warnings(parsed_response, ns):
407411
for warning_xml in all_warning_xml:
408412
warnings.append(warning_xml.get("message", None))
409413
return warnings
414+
415+
416+
def parse_batch_schedule_state(response: "Response", ns) -> list[str]:
417+
xml = fromstring(response.content)
418+
return [text for tag in xml.findall(".//t:scheduleLuid", namespaces=ns) if (text := tag.text)]

tableauserverclient/server/endpoint/schedules_endpoint.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
from collections.abc import Iterable
12
import copy
23
import logging
34
import warnings
45
from collections import namedtuple
5-
from typing import TYPE_CHECKING, Callable, Optional, Union
6+
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, overload
67

78
from .endpoint import Endpoint, api, parameter_added_in
89
from .exceptions import MissingRequiredFieldError
910
from tableauserverclient.server import RequestFactory
1011
from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem, ExtractItem
12+
from tableauserverclient.models.schedule_item import parse_batch_schedule_state
1113

1214
from tableauserverclient.helpers.logging import logger
1315

@@ -279,3 +281,48 @@ def get_extract_refresh_tasks(
279281
extract_items = ExtractItem.from_response(server_response.content, self.parent_srv.namespace)
280282

281283
return extract_items, pagination_item
284+
285+
@overload
286+
def batch_update_state(
287+
self,
288+
schedules: Iterable[ScheduleItem | str],
289+
state: Literal["active", "suspended"],
290+
update_all: Literal[False] = False,
291+
) -> list[str]: ...
292+
293+
@overload
294+
def batch_update_state(
295+
self, schedules: Any, state: Literal["active", "suspended"], update_all: Literal[True]
296+
) -> list[str]: ...
297+
298+
@api(version="3.27")
299+
def batch_update_state(self, schedules, state, update_all=False) -> list[str]:
300+
"""
301+
Batch update the status of one or more scheudles. If update_all is set,
302+
all schedules on the Tableau Server are affected.
303+
304+
Parameters
305+
----------
306+
schedules: Iterable[ScheudleItem | str] | Any
307+
The schedules to be updated. If update_all=True, this is ignored.
308+
309+
state: Literal["active", "suspended"]
310+
The state of the schedules, whether active or suspended.
311+
312+
update_all: bool
313+
Whether or not to apply the status to all schedules.
314+
315+
Returns
316+
-------
317+
List[str]
318+
The IDs of the affected schedules.
319+
"""
320+
params = {"state": state}
321+
if update_all:
322+
params["updateAll"] = "true"
323+
payload = RequestFactory.Empty.empty_req()
324+
else:
325+
payload = RequestFactory.Schedule.batch_update_state(schedules)
326+
327+
response = self.put_request(self.baseurl, payload, parameters={"params": params})
328+
return parse_batch_schedule_state(response, self.parent_srv.namespace)

tableauserverclient/server/request_factory.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,16 @@ def add_datasource_req(self, id_: Optional[str], task_type: str = TaskItem.Type.
643643
def add_flow_req(self, id_: Optional[str], task_type: str = TaskItem.Type.RunFlow) -> bytes:
644644
return self._add_to_req(id_, "flow", task_type)
645645

646+
@_tsrequest_wrapped
647+
def batch_update_state(self, xml: ET.Element, schedules: Iterable[ScheduleItem | str]) -> None:
648+
luids = ET.SubElement(xml, "scheduleLuids")
649+
for schedule in schedules:
650+
luid = getattr(schedule, "id", schedule)
651+
if not isinstance(luid, str):
652+
continue
653+
luid_tag = ET.SubElement(luids, "scheduleLuid")
654+
luid_tag.text = luid
655+
646656

647657
class SiteRequest:
648658
def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = None):
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<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">
2+
<scheduleLuids>
3+
<scheduleLuid>593d2ebf-0d18-4deb-9d21-b113a4902583</scheduleLuid>
4+
<scheduleLuid>cecbb71e-def0-4030-8068-5ae50f51db1c</scheduleLuid>
5+
<scheduleLuid>f39a6e7d-405e-4c07-8c18-95845f9da80e</scheduleLuid>
6+
</scheduleLuids>
7+
</tsResponse>

test/test_schedule.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22
from datetime import time
33

4+
from defusedxml.ElementTree import fromstring
45
import pytest
56
import requests_mock
67

@@ -26,7 +27,7 @@
2627
ADD_DATASOURCE_TO_SCHEDULE = TEST_ASSET_DIR / "schedule_add_datasource.xml"
2728
ADD_FLOW_TO_SCHEDULE = TEST_ASSET_DIR / "schedule_add_flow.xml"
2829
GET_EXTRACT_TASKS_XML = TEST_ASSET_DIR / "schedule_get_extract_refresh_tasks.xml"
29-
BATCH_UPDATE_STATE = TEST_ASSET_DIR / "schedules_batch_update_state.xml"
30+
BATCH_UPDATE_STATE = TEST_ASSET_DIR / "schedule_batch_update_state.xml"
3031

3132
WORKBOOK_GET_BY_ID_XML = TEST_ASSET_DIR / "workbook_get_by_id.xml"
3233
DATASOURCE_GET_BY_ID_XML = TEST_ASSET_DIR / "datasource_get_by_id.xml"
@@ -421,3 +422,58 @@ def test_get_extract_refresh_tasks(server: TSC.Server) -> None:
421422
assert isinstance(extracts[0], list)
422423
assert 2 == len(extracts[0])
423424
assert "task1" == extracts[0][0].id
425+
426+
427+
def test_batch_update_state_items(server: TSC.Server) -> None:
428+
server.version = "3.27"
429+
hourly_interval = TSC.HourlyInterval(start_time=time(2, 30), end_time=time(23, 0), interval_value=2)
430+
args = ("hourly", 50, TSC.ScheduleItem.Type.Extract, TSC.ScheduleItem.ExecutionOrder.Parallel, hourly_interval)
431+
new_schedules = [TSC.ScheduleItem(*args), TSC.ScheduleItem(*args), TSC.ScheduleItem(*args)]
432+
new_schedules[0]._id = "593d2ebf-0d18-4deb-9d21-b113a4902583"
433+
new_schedules[1]._id = "cecbb71e-def0-4030-8068-5ae50f51db1c"
434+
new_schedules[2]._id = "f39a6e7d-405e-4c07-8c18-95845f9da80e"
435+
436+
state = "active"
437+
with requests_mock.mock() as m:
438+
m.put(f"{server.schedules.baseurl}?state={state}", text=BATCH_UPDATE_STATE.read_text())
439+
resp = server.schedules.batch_update_state(new_schedules, state)
440+
441+
assert len(resp) == 3
442+
for sch, r in zip(new_schedules, resp):
443+
assert sch.id == r
444+
445+
446+
def test_batch_update_state_str(server: TSC.Server) -> None:
447+
server.version = "3.27"
448+
new_schedules = [
449+
"593d2ebf-0d18-4deb-9d21-b113a4902583",
450+
"cecbb71e-def0-4030-8068-5ae50f51db1c",
451+
"f39a6e7d-405e-4c07-8c18-95845f9da80e",
452+
]
453+
454+
state = "suspended"
455+
with requests_mock.mock() as m:
456+
m.put(f"{server.schedules.baseurl}?state={state}", text=BATCH_UPDATE_STATE.read_text())
457+
resp = server.schedules.batch_update_state(new_schedules, state)
458+
459+
assert len(resp) == 3
460+
for sch, r in zip(new_schedules, resp):
461+
assert sch == r
462+
463+
464+
def test_batch_update_state_all(server: TSC.Server) -> None:
465+
server.version = "3.27"
466+
new_schedules = [
467+
"593d2ebf-0d18-4deb-9d21-b113a4902583",
468+
"cecbb71e-def0-4030-8068-5ae50f51db1c",
469+
"f39a6e7d-405e-4c07-8c18-95845f9da80e",
470+
]
471+
472+
state = "suspended"
473+
with requests_mock.mock() as m:
474+
m.put(f"{server.schedules.baseurl}?state={state}&updateAll=true", text=BATCH_UPDATE_STATE.read_text())
475+
_ = server.schedules.batch_update_state(new_schedules, state, True)
476+
477+
history = m.request_history[0]
478+
479+
assert history.text == "<tsRequest />"

0 commit comments

Comments
 (0)