8000 Merge pull request #1328 from a-torres-2/schedules-multiple-intervals · daxcurson/server-client-python@e72552d · GitHub
[go: up one dir, main page]

Skip to content

Commit e72552d

Browse files
authored
Merge pull request tableau#1328 from a-torres-2/schedules-multiple-intervals
support for multiple intervals for hourly, daily, and monthly schedules
2 parents 666fcd8 + e8c9e0a commit e72552d

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
def __repr__(self):
3540
return f"<{self.__class__.__name__} start={self.start_time} end={self.end_time} interval={self.interval}>"
@@ -63,25 +68,44 @@ def interval(self):
6368
return self._interval
6469

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

72-
self._interval = interval
84+
self._interval = intervals
7385

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

84-
return [(interval_type, str(calculated_interval))]
106+
interval_type_pairs.append((interval_type, str(interval)))
107+
108+
return interval_type_pairs
85109

86110

87111
class DailyInterval(object):
@@ -111,8 +135,45 @@ def interval(self):
111135
return self._interval
112136

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

117178

118179
class WeeklyInterval(object):
@@ -155,7 +216,12 @@ def _interval_type_pairs(self):
155216
class MonthlyInterval(object):
156217
def __init__(self, start_time, interval_value):
157218
self.start_time = start_time
158-
self.interval = str(interval_value)
219+
220+
# interval should be a tuple, if it is not, assign as a tuple with single value
221+
if isinstance(interval_value, tuple):
222+
self.interval = interval_value
223+
else:
224+
self.interval = (interval_value,)
159225

160226
def __repr__(self):
161227
return f"<{self.__class__.__name__} start={self.start_time} interval={self.interval}>"
@@ -179,24 +245,24 @@ def interval(self):
179245
return self._interval
180246

181247
@interval.setter
182-
def interval(self, interval_value):
183-
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
184-
248+
def interval(self, interval_values):
185249
# This is weird because the value could be a str or an int
186250
# The only valid str is 'LastDay' so we check that first. If that's not it
187251
# try to convert it to an int, if that fails because it's an incorrect string
188252
# like 'badstring' we catch and re-raise. Otherwise we convert to int and check
189253
# that it's in range 1-31
254+
for interval_value in interval_values:
255+
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
190256

191-
if interval_value != "LastDay":
192-
try:
193-
if not (1 <= int(interval_value) <= 31):
194-
raise ValueError(error)
195-
except ValueError:
196-
if interval_value != "LastDay":
197-
raise ValueError(error)
257+
if interval_value != "LastDay":
258+
try:
259+
if not (1 <= int(interval_value) <= 31):
260+
raise ValueError(error)
261+
except ValueError:
262+
if interval_value != "LastDay":
263+
raise ValueError(error)
198264

199-
self._interval = str(interval_value)
265+
self._interval = interval_values
200266

201267
def _interval_type_pairs(self):
202268
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
@@ -254,25 +254,43 @@ def _parse_interval_item(parsed_response, frequency, ns):
254254
interval.extend(interval_elem.attrib.items())
255255

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

259271
if frequency == IntervalItem.Frequency.Hourly:
260-
interval_occurrence, interval_value = interval.pop()
272+
converted_intervals = []
261273

262-
# We use fractional hours for the two minute-based intervals.
263-
# Need to convert to hours from minutes here
264-
if interval_occurrence == IntervalItem.Occurrence.Minutes:
265-
interval_value = float(interval_value) / 60
274+
for i in interval:
275+
# We use fractional hours for the two minute-based intervals.
276+
# Need to convert to hours from minutes here
277+
if i[0] == IntervalItem.Occurrence.Minutes:
278+
converted_intervals.append(float(i[1]) / 60)
279+
elif i[0] == IntervalItem.Occurrence.Hours:
280+
converted_intervals.append(i[1])
281+
else:
282+
converted_intervals.append(i[1])
266283

267-
return HourlyInterval(start_time, end_time, interval_value)
284+
return HourlyInterval(start_time, end_time, tuple(converted_intervals))
268285

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

273290
if frequency == IntervalItem.Frequency.Monthly:
274-
interval_occurrence, interval_value = interval.pop()
275-
return MonthlyInterval(start_time, interval_value)
291+
interval_values = [i[1] for i in interval]
292+
293+
return MonthlyInterval(start_time, tuple(interval_values))
276294

277295
@staticmethod
278296
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