Skip to content

Commit

Permalink
Adding integration where public holidays will be skipped for the sche…
Browse files Browse the repository at this point in the history
…duling
  • Loading branch information
sylviamclaughlin authored Apr 27, 2024
1 parent ef2e3dd commit f4cf8b5
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 8 deletions.
27 changes: 26 additions & 1 deletion app/integrations/google_workspace/google_calendar.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from datetime import datetime, timedelta

import requests
import pytz

from integrations.google_workspace.google_service import (
Expand All @@ -14,6 +14,7 @@
SRE_BOT_EMAIL = os.environ.get("SRE_BOT_EMAIL")



@handle_google_api_errors
def get_freebusy(time_min, time_max, items, **kwargs):
"""Returns free/busy information for a set of calendars.
Expand Down Expand Up @@ -125,6 +126,9 @@ def find_first_available_slot(
busy_times.append((start, end))
busy_times.sort(key=lambda x: x[0])

# get the list of Canandian federal holidays
federal_holidays = get_federal_holidays()

for day_offset in range(days_in_future, days_in_future + search_days_limit):
# Calculate the start and end times of the search window for the current day
search_date = datetime.utcnow() + timedelta(days=day_offset)
Expand All @@ -140,6 +144,10 @@ def find_first_available_slot(
hour=19, minute=0, second=0, microsecond=0
) # 3 PM EST, times are in UTC

# if the day is a federal holiday, skip it
if search_date.date().strftime("%Y-%m-%d") in federal_holidays:
continue

# Attempt to find an available slot within this day's search window
for current_time in (
search_start + timedelta(minutes=i) for i in range(0, 121, duration_minutes)
Expand All @@ -153,3 +161,20 @@ def find_first_available_slot(
return current_time.astimezone(est), slot_end.astimezone(est)

return None, None # No available slot found after searching the limit

def get_federal_holidays():
# Get the public holidays for the current year
# Uses Paul Craig's Public holidays api to retrieve the federal holidays

# get today's year
year = datetime.now().year

# call the api to get the public holidays
url = f"https://canada-holidays.ca/api/v1/holidays?federal=true&year={year}"
response = requests.get(url)

# Store the observed dates of the holidays and return the list
holidays = []
for holiday in response.json()["holidays"]:
holidays.append(holiday["observedDate"])
return holidays
1 change: 1 addition & 0 deletions app/requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ freezegun==1.4.0
pytest==7.4.4
pytest-asyncio==0.23.6
pytest-env==0.8.2
requests-mock==1.12.1
108 changes: 101 additions & 7 deletions app/tests/integrations/google_workspace/test_google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def fixed_utc_now():
# Return a fixed UTC datetime
return datetime(2023, 4, 10, 12, 0) # This is a Monday


# Fixture to mock the datetime.now() function
@pytest.fixture
def mock_datetime_now(est_timezone):
Expand All @@ -69,6 +68,10 @@ def mock_datetime_now(est_timezone):
def items():
return [{"id": "calendar1"}, {"id": "calendar2"}]

# Fixture to mock the year
@pytest.fixture
def mock_year():
return 2024

@patch(
"integrations.google_workspace.google_calendar.DEFAULT_DELEGATED_ADMIN_EMAIL",
Expand Down Expand Up @@ -322,11 +325,12 @@ def test_insert_event_api_call_error(
assert mock_os_environ_get.called
assert not mock_handle_errors.called


@patch("integrations.google_workspace.google_calendar.get_federal_holidays")
@patch("integrations.google_workspace.google_calendar.datetime")
def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timezone):
def test_available_slot_on_first_weekday(mock_datetime, mock_federal_holidays ,fixed_utc_now, mock_year, est_timezone):
# Mock datetime to control the flow of time in the test
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.return_value.year = 2024
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)

Expand All @@ -343,7 +347,12 @@ def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timez
}
}
}

mock_federal_holidays.return_value = {"holidays": [
{"observedDate": "2024-01-01"},
{"observedDate": "2024-07-01"}
]
}

# Expected search date is three days in the future (which should be Thursday)
# Busy period is from 1 PM to 1:30 PM EST on the first day being checked (April 13th)
# The function should find an available slot after the busy period
Expand All @@ -363,10 +372,12 @@ def test_available_slot_on_first_weekday(mock_datetime, fixed_utc_now, est_timez


# Test out the find_first_available_slot function when multiple busy days
@patch("integrations.google_workspace.google_calendar.get_federal_holidays")
@patch("integrations.google_workspace.google_calendar.datetime")
def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezone):
def test_opening_exists_after_busy_days(mock_datetime, mock_federal_holidays, fixed_utc_now, est_timezone):
# Mock datetime to control the flow of time in the test
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.return_value.year = 2024
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
freebusy_response = {
Expand All @@ -382,6 +393,13 @@ def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezo
}
}

mock_federal_holidays.return_value = {"holidays": [
{"observedDate": "2024-01-01"},
{"observedDate": "2024-07-01"}
]
}


start, end = google_calendar.find_first_available_slot(
freebusy_response, days_in_future=3, duration_minutes=30, search_days_limit=60
)
Expand All @@ -396,8 +414,9 @@ def test_opening_exists_after_busy_days(mock_datetime, fixed_utc_now, est_timezo


# Test that weekends are skipped when searching for available slots
@patch("integrations.google_workspace.google_calendar.get_federal_holidays")
@patch("integrations.google_workspace.google_calendar.datetime")
def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone):
def test_skipping_weekends(mock_datetime, mock_federal_holidays, fixed_utc_now, est_timezone):
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
mock_datetime.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs)
Expand All @@ -411,6 +430,12 @@ def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone):
}
}

mock_federal_holidays.return_value = {"holidays": [
{"observedDate": "2024-01-01"},
{"observedDate": "2024-07-01"}
]
}

# For this test, ensure the mocked 'now' falls before a weekend, and verify that the function skips to the next weekday
start, end = google_calendar.find_first_available_slot(
freebusy_response, duration_minutes=30, days_in_future=1, search_days_limit=60
Expand All @@ -428,9 +453,10 @@ def test_skipping_weekends(mock_datetime, fixed_utc_now, est_timezone):


# Test that no available slots are found within the search limit
@patch("integrations.google_workspace.google_calendar.get_federal_holidays")
@patch("integrations.google_workspace.google_calendar.datetime")
def test_no_available_slots_within_search_limit(
mock_datetime, fixed_utc_now, est_timezone
mock_datetime, mock_federal_holidays, fixed_utc_now, est_timezone
):
mock_datetime.utcnow.return_value = fixed_utc_now
mock_datetime.fromisoformat.side_effect = lambda d: datetime.fromisoformat(d[:-1])
Expand All @@ -447,10 +473,78 @@ def test_no_available_slots_within_search_limit(
}
}

mock_federal_holidays.return_value = {"holidays": [
{"observedDate": "2024-01-01"},
{"observedDate": "2024-07-01"}
]
}

start, end = google_calendar.find_first_available_slot(
freebusy_response, duration_minutes=30, days_in_future=3, search_days_limit=60
)

assert (
start is None and end is None
), "Expected no available slots within the search limit"


# test that the federal holidays are correctly parsed
def test_get_federal_holidays(requests_mock):
# Mock the API response
mocked_response = {
"holidays": [
{"observedDate": "2024-01-01"},
{"observedDate": "2024-07-01"},
{"observedDate": "2024-12-25"}
]
}
requests_mock.get("https://canada-holidays.ca/api/v1/holidays?federal=true&year=2024", json=mocked_response)

# Call the function
holidays = google_calendar.get_federal_holidays()

# Assert that the holidays are correctly parsed
assert holidays == ["2024-01-01", "2024-07-01", "2024-12-25"]

# test that holidays are correctly fetched for a different year
def test_get_federal_holidays_with_different_year(requests_mock):
# Mock the API response for a different year
requests_mock.get("https://canada-holidays.ca/api/v1/holidays?federal=true&year=2025", json={"holidays": []})

# Patch datetime to control the current year
with patch('integrations.google_workspace.google_calendar.datetime') as mock_datetime:
mock_datetime.now.return_value = datetime(2025, 1, 1)

# Call the function
holidays = google_calendar.get_federal_holidays()

# Assert that no holidays are returned for the mocked year
assert holidays == []


# Test that an empty list is returned when there are no holidays
def test_api_returns_empty_list(requests_mock):
# Mock no holidays
requests_mock.get("https://canada-holidays.ca/api/v1/holidays?federal=true&year=2024", json={"holidays": []})

# Execute
holidays = google_calendar.get_federal_holidays()

# Verify that an empty list is correctly handled
assert holidays == [], "Expected an empty list when there are no holidays"


# Test that a leap year is correctly handled
def test_leap_year_handling(requests_mock):
# Mock response for a leap year with an extra day
requests_mock.get("https://canada-holidays.ca/api/v1/holidays?federal=true&year=2024", json={
"holidays": [
{"observedDate": "2024-02-29"} # Assuming this is a special leap year holiday
]
})

# Execute
holidays = google_calendar.get_federal_holidays()

# Verify leap year is considered
assert "2024-02-29" in holidays, "Leap year date should be included in the holidays"

0 comments on commit f4cf8b5

Please sign in to comment.