Skip to content

Commit

Permalink
Add support for all types of monthly repeating schedules (#1462)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bcantoni authored and jacalata committed Sep 17, 2024
1 parent 60e6b9c commit 28c2487
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 15 deletions.
41 changes: 27 additions & 14 deletions tableauserverclient/models/interval_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,21 +246,34 @@ def interval(self):

@interval.setter
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
# Valid monthly intervals strings can contain any of the following
# day numbers (1-31) (integer or string)
# relative day within the month (First, Second, ... Last)
# week days (Sunday, Monday, ... LastDay)
VALID_INTERVALS = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"LastDay",
"First",
"Second",
"Third",
"Fourth",
"Fifth",
"Last",
]
for value in range(1, 32):
VALID_INTERVALS.append(str(value))
VALID_INTERVALS.append(value)

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 not in VALID_INTERVALS:
error = f"Invalid monthly interval: {interval_value}"
raise ValueError(error)

self._interval = interval_values

Expand Down
4 changes: 3 additions & 1 deletion tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ def _make_request(

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

if content_type == "application/xml":
self.parent_srv._namespace.detect(server_response.content)
Expand Down
12 changes: 12 additions & 0 deletions test/assets/schedule_get_monthly_id_2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version='1.0' encoding='UTF-8'?>
<tsResponse
xmlns="http://tableau.com/api"
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">
<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">
<frequencyDetails start="08:00:00">
<intervals>
<interval weekDay="Monday" monthDay="First" />
</intervals>
</frequencyDetails>
</schedule>
</tsResponse>
16 changes: 16 additions & 0 deletions test/test_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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_MONTHLY_ID_2_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id_2.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")
Expand Down Expand Up @@ -158,6 +159,21 @@ def test_get_monthly_by_id(self) -> None:
self.assertEqual("Active", schedule.state)
self.assertEqual(("1", "2"), schedule.interval_item.interval)

def test_get_monthly_by_id_2(self) -> None:
self.server.version = "3.15"
with open(GET_MONTHLY_ID_2_XML, "rb") as f:
response_xml = f.read().decode("utf-8")
with requests_mock.mock() as m:
schedule_id = "8c5caf33-6223-4724-83c3-ccdc1e730a07"
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 First Monday!", schedule.name)
self.assertEqual("Active", schedule.state)
self.assertEqual(("Monday", "First"), 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)
Expand Down

0 comments on commit 28c2487

Please sign in to comment.