diff --git a/tableauserverclient/models/interval_item.py b/tableauserverclient/models/interval_item.py
index 02b57591b..f2f159625 100644
--- a/tableauserverclient/models/interval_item.py
+++ b/tableauserverclient/models/interval_item.py
@@ -29,7 +29,12 @@ class HourlyInterval(object):
def __init__(self, start_time, end_time, interval_value):
self.start_time = start_time
self.end_time = end_time
- self.interval = interval_value
+
+ # interval should be a tuple, if it is not, assign as a tuple with single value
+ if isinstance(interval_value, tuple):
+ self.interval = interval_value
+ else:
+ self.interval = (interval_value,)
def __repr__(self):
return f"<{self.__class__.__name__} start={self.start_time} end={self.end_time} interval={self.interval}>"
@@ -63,25 +68,44 @@ def interval(self):
return self._interval
@interval.setter
- def interval(self, interval):
+ def interval(self, intervals):
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
- if float(interval) not in VALID_INTERVALS:
- error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
- raise ValueError(error)
+ for interval in intervals:
+ # if an hourly interval is a string, then it is a weekDay interval
+ if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
+ error = "Invalid weekDay interval {}".format(interval)
+ raise ValueError(error)
+
+ # if an hourly interval is a number, it is an hours or minutes interval
+ if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
+ raise ValueError(error)
- self._interval = interval
+ self._interval = intervals
def _interval_type_pairs(self):
- # We use fractional hours for the two minute-based intervals.
- # Need to convert to minutes from hours here
- if self.interval in {0.25, 0.5}:
- calculated_interval = int(self.interval * 60)
- interval_type = IntervalItem.Occurrence.Minutes
- else:
- calculated_interval = self.interval
- interval_type = IntervalItem.Occurrence.Hours
+ interval_type_pairs = []
+ for interval in self.interval:
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to minutes from hours here
+ if interval in {0.25, 0.5}:
+ calculated_interval = int(interval * 60)
+ interval_type = IntervalItem.Occurrence.Minutes
+
+ interval_type_pairs.append((interval_type, str(calculated_interval)))
+ else:
+ # if the interval is a non-numeric string, it will always be a weekDay
+ if isinstance(interval, str) and not interval.isnumeric():
+ interval_type = IntervalItem.Occurrence.WeekDay
+
+ interval_type_pairs.append((interval_type, str(interval)))
+ # otherwise the interval is hours
+ else:
+ interval_type = IntervalItem.Occurrence.Hours
- return [(interval_type, str(calculated_interval))]
+ interval_type_pairs.append((interval_type, str(interval)))
+
+ return interval_type_pairs
class DailyInterval(object):
@@ -111,8 +135,45 @@ def interval(self):
return self._interval
@interval.setter
- def interval(self, interval):
- self._interval = interval
+ def interval(self, intervals):
+ VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
+
+ for interval in intervals:
+ # if an hourly interval is a string, then it is a weekDay interval
+ if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
+ error = "Invalid weekDay interval {}".format(interval)
+ raise ValueError(error)
+
+ # if an hourly interval is a number, it is an hours or minutes interval
+ if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
+ raise ValueError(error)
+
+ self._interval = intervals
+
+ def _interval_type_pairs(self):
+ interval_type_pairs = []
+ for interval in self.interval:
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to minutes from hours here
+ if interval in {0.25, 0.5}:
+ calculated_interval = int(interval * 60)
+ interval_type = IntervalItem.Occurrence.Minutes
+
+ interval_type_pairs.append((interval_type, str(calculated_interval)))
+ else:
+ # if the interval is a non-numeric string, it will always be a weekDay
+ if isinstance(interval, str) and not interval.isnumeric():
+ interval_type = IntervalItem.Occurrence.WeekDay
+
+ interval_type_pairs.append((interval_type, str(interval)))
+ # otherwise the interval is hours
+ else:
+ interval_type = IntervalItem.Occurrence.Hours
+
+ interval_type_pairs.append((interval_type, str(interval)))
+
+ return interval_type_pairs
class WeeklyInterval(object):
@@ -155,7 +216,12 @@ def _interval_type_pairs(self):
class MonthlyInterval(object):
def __init__(self, start_time, interval_value):
self.start_time = start_time
- self.interval = str(interval_value)
+
+ # interval should be a tuple, if it is not, assign as a tuple with single value
+ if isinstance(interval_value, tuple):
+ self.interval = interval_value
+ else:
+ self.interval = (interval_value,)
def __repr__(self):
return f"<{self.__class__.__name__} start={self.start_time} interval={self.interval}>"
@@ -179,24 +245,24 @@ def interval(self):
return self._interval
@interval.setter
- def interval(self, interval_value):
- error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
-
+ def interval(self, interval_values):
# This is weird because the value could be a str or an int
# The only valid str is 'LastDay' so we check that first. If that's not it
# try to convert it to an int, if that fails because it's an incorrect string
# like 'badstring' we catch and re-raise. Otherwise we convert to int and check
# that it's in range 1-31
+ for interval_value in interval_values:
+ error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
- if interval_value != "LastDay":
- try:
- if not (1 <= int(interval_value) <= 31):
- raise ValueError(error)
- except ValueError:
- if interval_value != "LastDay":
- raise ValueError(error)
+ if interval_value != "LastDay":
+ try:
+ if not (1 <= int(interval_value) <= 31):
+ raise ValueError(error)
+ except ValueError:
+ if interval_value != "LastDay":
+ raise ValueError(error)
- self._interval = str(interval_value)
+ self._interval = interval_values
def _interval_type_pairs(self):
return [(IntervalItem.Occurrence.MonthDay, self.interval)]
diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py
index dc0eca948..db187a5f9 100644
--- a/tableauserverclient/models/schedule_item.py
+++ b/tableauserverclient/models/schedule_item.py
@@ -254,25 +254,43 @@ def _parse_interval_item(parsed_response, frequency, ns):
interval.extend(interval_elem.attrib.items())
if frequency == IntervalItem.Frequency.Daily:
- return DailyInterval(start_time)
+ converted_intervals = []
+
+ for i in interval:
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to hours from minutes here
+ if i[0] == IntervalItem.Occurrence.Minutes:
+ converted_intervals.append(float(i[1]) / 60)
+ elif i[0] == IntervalItem.Occurrence.Hours:
+ converted_intervals.append(float(i[1]))
+ else:
+ converted_intervals.append(i[1])
+
+ return DailyInterval(start_time, *converted_intervals)
if frequency == IntervalItem.Frequency.Hourly:
- interval_occurrence, interval_value = interval.pop()
+ converted_intervals = []
- # We use fractional hours for the two minute-based intervals.
- # Need to convert to hours from minutes here
- if interval_occurrence == IntervalItem.Occurrence.Minutes:
- interval_value = float(interval_value) / 60
+ for i in interval:
+ # We use fractional hours for the two minute-based intervals.
+ # Need to convert to hours from minutes here
+ if i[0] == IntervalItem.Occurrence.Minutes:
+ converted_intervals.append(float(i[1]) / 60)
+ elif i[0] == IntervalItem.Occurrence.Hours:
+ converted_intervals.append(i[1])
+ else:
+ converted_intervals.append(i[1])
- return HourlyInterval(start_time, end_time, interval_value)
+ return HourlyInterval(start_time, end_time, tuple(converted_intervals))
if frequency == IntervalItem.Frequency.Weekly:
interval_values = [i[1] for i in interval]
return WeeklyInterval(start_time, *interval_values)
if frequency == IntervalItem.Frequency.Monthly:
- interval_occurrence, interval_value = interval.pop()
- return MonthlyInterval(start_time, interval_value)
+ interval_values = [i[1] for i in interval]
+
+ return MonthlyInterval(start_time, tuple(interval_values))
@staticmethod
def _parse_element(schedule_xml, ns):
diff --git a/test/assets/schedule_get_daily_id.xml b/test/assets/schedule_get_daily_id.xml
new file mode 100644
index 000000000..99467a391
--- /dev/null
+++ b/test/assets/schedule_get_daily_id.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_get_hourly_id.xml b/test/assets/schedule_get_hourly_id.xml
new file mode 100644
index 000000000..27c374ccf
--- /dev/null
+++ b/test/assets/schedule_get_hourly_id.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/schedule_get_monthly_id.xml b/test/assets/schedule_get_monthly_id.xml
new file mode 100644
index 000000000..3fc32cc57
--- /dev/null
+++ b/test/assets/schedule_get_monthly_id.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/test_schedule.py b/test/test_schedule.py
index 807467918..76c8720b9 100644
--- a/test/test_schedule.py
+++ b/test/test_schedule.py
@@ -11,6 +11,9 @@
GET_XML = os.path.join(TEST_ASSET_DIR, "schedule_get.xml")
GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_by_id.xml")
+GET_HOURLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_hourly_id.xml")
+GET_DAILY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_daily_id.xml")
+GET_MONTHLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id.xml")
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml")
CREATE_HOURLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_hourly.xml")
CREATE_DAILY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_daily.xml")
@@ -100,6 +103,51 @@ def test_get_by_id(self) -> None:
self.assertEqual("Weekday early mornings", schedule.name)
self.assertEqual("Active", schedule.state)
+ def test_get_hourly_by_id(self) -> None:
+ self.server.version = "3.8"
+ with open(GET_HOURLY_ID_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 = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
+ m.get(baseurl, text=response_xml)
+ schedule = self.server.schedules.get_by_id(schedule_id)
+ self.assertIsNotNone(schedule)
+ self.assertEqual(schedule_id, schedule.id)
+ self.assertEqual("Hourly schedule", schedule.name)
+ self.assertEqual("Active", schedule.state)
+ self.assertEqual(("Monday", 0.5), schedule.interval_item.interval)
+
+ def test_get_daily_by_id(self) -> None:
+ self.server.version = "3.8"
+ with open(GET_DAILY_ID_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 = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
+ m.get(baseurl, text=response_xml)
+ schedule = self.server.schedules.get_by_id(schedule_id)
+ self.assertIsNotNone(schedule)
+ self.assertEqual(schedule_id, schedule.id)
+ self.assertEqual("Daily schedule", schedule.name)
+ self.assertEqual("Active", schedule.state)
+ self.assertEqual(("Monday", 2.0), schedule.interval_item.interval)
+
+ def test_get_monthly_by_id(self) -> None:
+ self.server.version = "3.8"
+ with open(GET_MONTHLY_ID_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 = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
+ m.get(baseurl, text=response_xml)
+ schedule = self.server.schedules.get_by_id(schedule_id)
+ self.assertIsNotNone(schedule)
+ self.assertEqual(schedule_id, schedule.id)
+ self.assertEqual("Monthly multiple days", schedule.name)
+ self.assertEqual("Active", schedule.state)
+ self.assertEqual(("1", "2"), schedule.interval_item.interval)
+
def test_delete(self) -> None:
with requests_mock.mock() as m:
m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204)
@@ -131,7 +179,7 @@ def test_create_hourly(self) -> None:
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
self.assertEqual(time(2, 30), new_schedule.interval_item.start_time)
self.assertEqual(time(23), new_schedule.interval_item.end_time) # type: ignore[union-attr]
- self.assertEqual("8", new_schedule.interval_item.interval) # type: ignore[union-attr]
+ self.assertEqual(("8",), new_schedule.interval_item.interval) # type: ignore[union-attr]
def test_create_daily(self) -> None:
with open(CREATE_DAILY_XML, "rb") as f:
@@ -216,7 +264,7 @@ def test_create_monthly(self) -> None:
self.assertEqual("2016-10-12T14:00:00Z", format_datetime(new_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
self.assertEqual(time(7), new_schedule.interval_item.start_time)
- self.assertEqual("12", new_schedule.interval_item.interval) # type: ignore[union-attr]
+ self.assertEqual(("12",), new_schedule.interval_item.interval) # type: ignore[union-attr]
def test_update(self) -> None:
with open(UPDATE_XML, "rb") as f: