8000 add support for multiple intervals for hourly, daily, and monthly sch… · daxcurson/server-client-python@f17a75d · GitHub
[go: up one dir, main page]

Skip to content

Commit f17a75d

Browse files
committed
add support for multiple intervals for hourly, daily, and monthly schedules
1 parent 01e0372 commit f17a75d

File tree

6 files changed

+205
-40
lines changed

6 files changed

+205
-40
lines changed

tableauserverclient/models/interval_item.py

Lines changed: 95 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ class HourlyInterval(object):
2929
def __init__(self, start_time, end_time, interval_value):
3030
self.start_time = start_time
3131
self.end_time = end_time
32-
self.interval = interval_value
32+
33+
# interval should be a tuple, if it is not, assign as a tuple with single value
34+
if isinstance(interval_value, tuple):
35+
self.interval = interval_value
36+
else:
37+
self.interval = (interval_value,)
3338

3439
@property
3540
def _frequency(self):
@@ -60,25 +65,44 @@ def interval(self):
6065
return self._interval
6166

6267
@interval.setter
63-
def interval(self, interval):
68+
def interval(self, intervals):
6469
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
65-
if float(interval) not in VALID_INTERVALS:
66-
error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
67-
raise ValueError(error)
70+
for interval in intervals:
71+
# if an hourly interval is a string, then it is a weekDay interval
72+
if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
73+
error = "Invalid weekDay interval {}".format(interval)
74+
raise ValueError(error)
75+
76+
# if an hourly interval is a number, it is an hours or minutes interval
77+
if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
78+
error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
79+
raise ValueError(error)
6880

69-
self._interval = interval
81+
self._interval = intervals
7082

7183
def _interval_type_pairs(self):
72-
# We use fractional hours for the two minute-based intervals.
73-
# Need to convert to minutes from hours here
74-
if self.interval in {0.25, 0.5}:
75-
calculated_interval = int(self.interval * 60)
76-
interval_type = IntervalItem.Occurrence.Minutes
77-
else:
78-
calculated_interval = self.interval
79-
interval_type = IntervalItem.Occurrence.Hours
84+
interval_type_pairs = []
85+
for interval in self.interval:
86+
# We use fractional hours for the two minute-based intervals.
87+
# Need to convert to minutes from hours here
88+
if interval in {0.25, 0.5}:
89+
calculated_interval = int(interval * 60)
90+
interval_type = IntervalItem.Occurrence.Minutes
91+
92+
interval_type_pairs.append((interval_type, str(calculated_interval)))
93+
else:
94+
# if the interval is a non-numeric string, it will always be a weekDay
95+
if isinstance(interval, str) and not interval.isnumeric():
96+
interval_type = IntervalItem.Occurrence.WeekDay
97+
98+
interval_type_pairs.append((interval_type, str(interval)))
99+
# otherwise the interval is hours
100+
else:
101+
interval_type = IntervalItem.Occurrence.Hours
80102

81-
return [(interval_type, str(calculated_interval))]
103+
interval_type_pairs.append((interval_type, str(interval)))
104+
105+
return interval_type_pairs
82106

83107

84108
class DailyInterval(object):
@@ -105,8 +129,45 @@ def interval(self):
105129
return self._interval
106130

107131
@interval.setter
108-
def interval(self, interval):
109-
self._interval = interval
132+
def interval(self, intervals):
133+
VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
134+
135+
for interval in intervals:
136+
# if an hourly interval is a string, then it is a weekDay interval
137+
if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
138+
error = "Invalid weekDay interval {}".format(interval)
139+
raise ValueError(error)
140+
141+
# if an hourly interval is a number, it is an hours or minutes interval
142+
if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS:
143+
error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
144+
raise ValueError(error)
145+
146+
self._interval = intervals
147+
148+
def _interval_type_pairs(self):
149+
interval_type_pairs = []
150+
for interval in self.interval:
151+
# We use fractional hours for the two minute-based intervals.
152+
# Need to convert to minutes from hours here
153+
if interval in {0.25, 0.5}:
154+
calculated_interval = int(interval * 60)
155+
interval_type = IntervalItem.Occurrence.Minutes
156+
157+
interval_type_pairs.append((interval_type, str(calculated_interval)))
158+
else:
159+
# if the interval is a non-numeric string, it will always be a weekDay
160+
if isinstance(interval, str) and not interval.isnumeric():
161+
interval_type = IntervalItem.Occurrence.WeekDay
162+
163+
interval_type_pairs.append((interval_type, str(interval)))
164+
# otherwise the interval is hours
165+
else:
166+
interval_type = IntervalItem.Occurrence.Hours
167+
168+
interval_type_pairs.append((interval_type, str(interval)))
169+
170+
return interval_type_pairs
110171

111172

112173
class WeeklyInterval(object):
@@ -146,7 +207,12 @@ def _interval_type_pairs(self):
146207
class MonthlyInterval(object):
147208
def __init__(self, start_time, interval_value):
148209
self.start_time = start_time
149-
self.interval = str(interval_value)
210+
211+
# interval should be a tuple, if it is not, assign as a tuple with single value
212+
if isinstance(interval_value, tuple):
213+
self.interval = interval_value
214+
else:
215+
self.interval = (interval_value,)
150216

151217
@property
152218
def _frequency(self):
@@ -167,24 +233,24 @@ def interval(self):
167233
return self._interval
168234

169235
@interval.setter
170-
def interval(self, interval_value):
171-
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
172-
236+
def interval(self, interval_values):
173237
# This is weird because the value could be a str or an int
174238
# The only valid str is 'LastDay' so we check that first. If that's not it
175239
# try to convert it to an int, if that fails because it's an incorrect string
176240
# like 'badstring' we catch and re-raise. Otherwise we convert to int and check
177241
# that it's in range 1-31
242+
for interval_value in interval_values:
243+
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
178244

179-
if interval_value != "LastDay":
180-
try:
181-
if not (1 <= int(interval_value) <= 31):
182-
raise ValueError(error)
183-
except ValueError:
184-
if interval_value != "LastDay":
185-
raise ValueError(error)
245+
if interval_value != "LastDay":
246+
try:
247+
if not (1 <= int(interval_value) <= 31):
248+
raise ValueError(error)
249+
except ValueError:
250+
if interval_value != "LastDay":
251+
raise ValueError(error)
186252

187-
self._interval = str(interval_value)
253+
self._interval = interval_values
188254

189255
def _interval_type_pairs(self):
190256
return [(IntervalItem.Occurrence.MonthDay, self.interval)]

tableauserverclient/models/schedule_item.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -251,25 +251,43 @@ def _parse_interval_item(parsed_response, frequency, ns):
251251
interval.extend(interval_elem.attrib.items())
252252

253253
if frequency == IntervalItem.Frequency.Daily:
254-
return DailyInterval(start_time)
254+
converted_intervals = []
255+
256+
for i in interval:
257+
# We use fractional hours for the two minute-based intervals.
258+
# Need to convert to hours from minutes here
259+
if i[0] == IntervalItem.Occurrence.Minutes:
260+
converted_intervals.append(float(i[1]) / 60)
261+
elif i[0] == IntervalItem.Occurrence.Hours:
262+
converted_intervals.append(float(i[1]))
263+
else:
264+
converted_intervals.append(i[1])
265+
266+
return DailyInterval(start_time, *converted_intervals)
255267

256268
if frequency == IntervalItem.Frequency.Hourly:
257-
interval_occurrence, interval_value = interval.pop()
269+
converted_intervals = []
258270

259-
# We use fractional hours for the two minute-based intervals.
260-
# Need to convert to hours from minutes here
261-
if interval_occurrence == IntervalItem.Occurrence.Minutes:
262-
interval_value = float(interval_value) / 60
271+
for i in interval:
272+
# We use fractional hours for the two minute-based intervals.
273+
# Need to convert to hours from minutes here
274+
if i[0] == IntervalItem.Occurrence.Minutes:
275+
converted_intervals.append(float(i[1]) / 60)
276+
elif i[0] == IntervalItem.Occurrence.Hours:
277+
converted_intervals.append(i[1])
278+
else:
279+
converted_intervals.append(i[1])
263280

264-
return HourlyInterval(start_time, end_time, interval_value)
281+
return HourlyInterval(start_time, end_time, tuple(converted_intervals))
265282

266283
if frequency == IntervalItem.Frequency.Weekly:
267284
interval_values = [i[1] for i in interval]
268285
return WeeklyInterval(start_time, *interval_values)
269286

270287
if frequency == IntervalItem.Frequency.Monthly:
271-
interval_occurrence, interval_value = interval.pop()
272-
return MonthlyInterval(start_time, interval_value)
288+
interval_values = [i[1] for i in interval]
289+
290+
return MonthlyInterval(start_time, tuple(interval_values))
273291

274292
@staticmethod
275293
def _parse_element(schedule_xml, ns):

test/assets/schedule_get_daily_id.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
<schedule id="c9cff7f9-309c-4361-99ff-d4ba8c9f5467" name="Daily schedule" state="Active" priority="50" createdAt="2016-07-06T20:19:00Z" updatedAt="2016-09-13T11:00:32Z" type="Extract" frequency="Daily" nextRunAt="2016-09-14T11:00:00Z">
4+
<frequencyDetails start="14:00:00" end="01:00:00">
5+
<intervals>
6+
<interval weekDay="Monday"/>
7+
<interval hours="2"/>
8+
</intervals>
9+
</frequencyDetails>
10+
</schedule>
11+
</tsResponse>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
<schedule id="c9cff7f9-309c-4361-99ff-d4ba8c9f5467" name="Hourly schedule" state="Active" priority="50" createdAt="2016-07-06T20:19:00Z" updatedAt="2016-09-13T11:00:32Z" type="Extract" frequency="Hourly" nextRunAt="2016-09-14T11:00:00Z">
4+
<frequencyDetails start="14:00:00" end="01:00:00">
5+
<intervals>
6+
<interval weekDay="Monday"/>
7+
<interval minutes="30"/>
8+
</intervals>
9+
</frequencyDetails>
10+
</schedule>
11+
</tsResponse>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
<schedule id="c9cff7f9-309c-4361-99ff-d4ba8c9f5467" name="Monthly multiple days" state="Active" priority="50" createdAt="2016-07-06T20:19:00Z" updatedAt="2016-09-13T11:00:32Z" type="Extract" frequency="Monthly" nextRunAt="2016-09-14T11:00:00Z">
4+
<frequencyDetails start="14:00:00">
5+
<intervals>
6+
<interval monthDay="1"/>
7+
<interval monthDay="2"/>
8+
</intervals>
9+
</frequencyDetails>
10+
</schedule>
11+
</tsResponse>

test/test_schedule.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
GET_XML = os.path.join(TEST_ASSET_DIR, "schedule_get.xml")
1313
GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_by_id.xml")
14+
GET_HOURLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_hourly_id.xml")
15+
GET_DAILY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_daily_id.xml")
16+
GET_MONTHLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id.xml")
1417
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml")
1518
CREATE_HOURLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_hourly.xml")
1619
CREATE_DAILY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_daily.xml")
@@ -100,6 +103,51 @@ def test_get_by_id(self) -> None:
100103
self.assertEqual("Weekday early mornings", schedule.name)
101104
self.assertEqual("Active", schedule.state)
102105

106+
def test_get_hourly_by_id(self) -> None:
107+
self.server.version = "3.8"
108+
with open(GET_HOURLY_ID_XML, "rb") as f:
109+
response_xml = f.read().decode("utf-8")
110+
with requests_mock.mock() as m:
111+
schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467"
112+
baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
113+
m.get(baseurl, text=response_xml)
114+
schedule = self.server.schedules.get_by_id(schedule_id)
115+
self.assertIsNotNone(schedule)
116+
self.assertEqual(schedule_id, schedule.id)
117+
self.assertEqual("Hourly schedule", schedule.name)
118+
self.assertEqual("Active", schedule.state)
119+
self.assertEqual(("Monday", 0.5), schedule.interval_item.interval)
120+
121+
def test_get_daily_by_id(self) -> None:
122+
self.server.version = "3.8"
123+
with open(GET_DAILY_ID_XML, "rb") as f:
124+
response_xml = f.read().decode("utf-8")
125+
with requests_mock.mock() as m:
126+
schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467"
127+
baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
128+
m.get(baseurl, text=response_xml)
129+
schedule = self.server.schedules.get_by_id(schedule_id)
130+
self.assertIsNotNone(schedule)
131+
self.assertEqual(schedule_id, schedule.id)
132+
self.assertEqual("Daily schedule", schedule.name)
133+
self.assertEqual("Active", schedule.state)
134+
self.assertEqual(("Monday", 2.0), schedule.interval_item.interval)
135+
136+
def test_get_monthly_by_id(self) -> None:
137+
self.server.version = "3.8"
138+
with open(GET_MONTHLY_ID_XML, "rb") as f:
139+
response_xml = f.read().decode("utf-8")
140+
with requests_mock.mock() as m:
141+
schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467"
142+
baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
143+
m.get(baseurl, text=response_xml)
144+
schedule = self.server.schedules.get_by_id(schedule_id)
145+
self.assertIsNotNone(schedule)
146+
self.assertEqual(schedule_id, schedule.id)
147+
self.assertEqual("Monthly multiple days", schedule.name)
148+
self.assertEqual("Active", schedule.state)
149+
self.assertEqual(("1", "2"), schedule.interval_item.interval)
150+
103151
def test_delete(self) -> None:
104152
with requests_mock.mock() as m:
105153
m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204)
@@ -131,7 +179,7 @@ def test_create_hourly(self) -> None:
131179
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
132180
self.assertEqual(time(2, 30), new_schedule.interval_item.start_time)
133181
self.assertEqual(time(23), new_schedule.interval_item.end_time) # type: ignore[union-attr]
134-
self.assertEqual("8", new_schedule.interval_item.interval) # type: ignore[union-attr]
182+
self.assertEqual(("8",), new_schedule.interval_item.interval) # type: ignore[union-attr]
135183

136184
def test_create_daily(self) -> None:
137185
with open(CREATE_DAILY_XML, "rb") as f:
@@ -216,7 +264,7 @@ def test_create_monthly(self) -> None:
216264
self.assertEqual("2016-10-12T14:00:00Z", format_datetime(new_schedule.next_run_at))
217265
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
218266
self.assertEqual(time(7), new_schedule.interval_item.start_time)
219-
self.assertEqual("12", new_schedule.interval_item.interval) # type: ignore[union-attr]
267+
self.assertEqual(("12",), new_schedule.interval_item.interval) # type: ignore[union-attr]
220268

221269
def test_update(self) -> None:
222270
with open(UPDATE_XML, "rb") as f:

0 commit comments

Comments
 (0)
0