Skip to content

Commit 28c2487

Browse files
bcantonijacalata
authored andcommitted
Add support for all types of monthly repeating schedules (#1462)
Previously we just handled monthly schedules which repeated on a day (1-31) or 'LastDay'. Tableau Server has since added more options such as "first Monday". This change catches up the interval validation to match what might be received from the server. Fixes #1358 * Add failing test for "monthly on first Monday" schedule * Add support for all monthly schedule variations * Unrelated fix for debug logging of API responses and add a small warning
1 parent 60e6b9c commit 28c2487

File tree

4 files changed

+58
-15
lines changed

4 files changed

+58
-15
lines changed

tableauserverclient/models/interval_item.py

+27-14
Original file line numberDiff line numberDiff line change
@@ -246,21 +246,34 @@ def interval(self):
246246

247247
@interval.setter
248248
def interval(self, interval_values):
249-
# This is weird because the value could be a str or an int
250-
# The only valid str is 'LastDay' so we check that first. If that's not it
251-
# try to convert it to an int, if that fails because it's an incorrect string
252-
# like 'badstring' we catch and re-raise. Otherwise we convert to int and check
253-
# that it's in range 1-31
249+
# Valid monthly intervals strings can contain any of the following
250+
# day numbers (1-31) (integer or string)
251+
# relative day within the month (First, Second, ... Last)
252+
# week days (Sunday, Monday, ... LastDay)
253+
VALID_INTERVALS = [
254+
"Sunday",
255+
"Monday",
256+
"Tuesday",
257+
"Wednesday",
258+
"Thursday",
259+
"Friday",
260+
"Saturday",
261+
"LastDay",
262+
"First",
263+
"Second",
264+
"Third",
265+
"Fourth",
266+
"Fifth",
267+
"Last",
268+
]
269+
for value in range(1, 32):
270+
VALID_INTERVALS.append(str(value))
271+
VALID_INTERVALS.append(value)
272+
254273
for interval_value in interval_values:
255-
error = "Invalid interval value for a monthly frequency: {}.".format(interval_value)
256-
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)
274+
if interval_value not in VALID_INTERVALS:
275+
error = f"Invalid monthly interval: {interval_value}"
276+
raise ValueError(error)
264277

265278
self._interval = interval_values
266279

tableauserverclient/server/endpoint/endpoint.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ def _make_request(
144144

145145
loggable_response = self.log_response_safely(server_response)
146146
logger.debug("Server response from {0}".format(url))
147-
# logger.debug("\n\t{1}".format(loggable_response))
147+
# uncomment the following to log full responses in debug mode
148+
# BE CAREFUL WHEN SHARING THESE RESULTS - MAY CONTAIN YOUR SENSITIVE DATA
149+
# logger.debug(loggable_response)
148150

149151
if content_type == "application/xml":
150152
self.parent_srv._namespace.detect(server_response.content)
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse
3+
xmlns="http://tableau.com/api"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_25.xsd">
5+
<schedule id="8c5caf33-6223-4724-83c3-ccdc1e730a07" name="Monthly First Monday!" state="Active" priority="50" createdAt="2024-09-12T01:22:07Z" updatedAt="2024-09-12T01:22:48Z" type="Extract" frequency="Monthly" nextRunAt="2024-10-07T08:00:00Z" executionOrder="Parallel">
6+
<frequencyDetails start="08:00:00">
7+
<intervals>
8+
<interval weekDay="Monday" monthDay="First" />
9+
</intervals>
10+
</frequencyDetails>
11+
</schedule>
12+
</tsResponse>

test/test_schedule.py

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
GET_HOURLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_hourly_id.xml")
1515
GET_DAILY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_daily_id.xml")
1616
GET_MONTHLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id.xml")
17+
GET_MONTHLY_ID_2_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id_2.xml")
1718
GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml")
1819
CREATE_HOURLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_hourly.xml")
1920
CREATE_DAILY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_daily.xml")
@@ -158,6 +159,21 @@ def test_get_monthly_by_id(self) -> None:
158159
self.assertEqual("Active", schedule.state)
159160
self.assertEqual(("1", "2"), schedule.interval_item.interval)
160161

162+
def test_get_monthly_by_id_2(self) -> None:
163+
self.server.version = "3.15"
164+
with open(GET_MONTHLY_ID_2_XML, "rb") as f:
165+
response_xml = f.read().decode("utf-8")
166+
with requests_mock.mock() as m:
167+
schedule_id = "8c5caf33-6223-4724-83c3-ccdc1e730a07"
168+
baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
169+
m.get(baseurl, text=response_xml)
170+
schedule = self.server.schedules.get_by_id(schedule_id)
171+
self.assertIsNotNone(schedule)
172+
self.assertEqual(schedule_id, schedule.id)
173+
self.assertEqual("Monthly First Monday!", schedule.name)
174+
self.assertEqual("Active", schedule.state)
175+
self.assertEqual(("Monday", "First"), schedule.interval_item.interval)
176+
161177
def test_delete(self) -> None:
162178
with requests_mock.mock() as m:
163179
m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204)

0 commit comments

Comments
 (0)