Last active
May 14, 2025 07:27
-
-
Save dceoy/57e6108cee98ac06bb2ed33ab80688b4 to your computer and use it in GitHub Desktop.
[Python] Check if now is any event in iCalendar data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Utilities for iCalendar (ICS) data.""" | |
from datetime import datetime | |
from zoneinfo import ZoneInfo | |
import recurring_ical_events # type: ignore[reportMissingTypeStubs] | |
from aws_lambda_powertools import Logger | |
from icalendar import Calendar | |
logger = Logger() | |
def check_if_now_in_event(ics: str, timezone: str = "Asia/Tokyo") -> bool: | |
"""Check if the current time is within at least one VEVENT in an iCalendar string. | |
Args: | |
ics (str): iCalendar string. | |
timezone (str | None): IANA time-zone. Defaults to "Asia/Tokyo". | |
Returns: | |
bool: Whether the current time is within any VEVENT in the iCalendar string. | |
""" | |
logger.info("ics: %s", ics) | |
if not ics: | |
logger.info("No iCalendar data") | |
return False | |
else: | |
now = datetime.now(ZoneInfo(timezone)) | |
logger.info("now: %s", now) | |
try: | |
events = recurring_ical_events.of(Calendar.from_ical(ics)).at(now) | |
except Exception: | |
logger.exception("Error parsing iCalendar data") | |
return False | |
else: | |
logger.info("events: %s", events) | |
return len(events) > 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Unit tests for ics module.""" | |
import pytest | |
from freezegun import freeze_time | |
from ics import check_if_now_in_event | |
_MEETING_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;TZID=Asia/Tokyo:20250419T090000 | |
DTEND;TZID=Asia/Tokyo:20250419T100000 | |
SUMMARY:Morning meeting | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_ALL_DAY_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;VALUE=DATE:20250419 | |
DTEND;VALUE=DATE:20250420 | |
SUMMARY:Holiday | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_EMPTY_ICS = """\ | |
BEGIN:VCALENDAR | |
END:VCALENDAR | |
""" | |
_DURATION_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;TZID=Asia/Tokyo:20250419T130000 | |
DURATION:PT1H | |
SUMMARY:Lunch meeting | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_NO_END_OR_DURATION_DATE_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;TZID=DATE:20250419 | |
SUMMARY:Quick check-in | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_NO_END_OR_DURATION_DATETIME_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;TZID=Asia/Tokyo:20250419T150000 | |
SUMMARY:Quick check-in | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_NO_SUMMARY_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
UID:[email protected] | |
DTSTART;TZID=Asia/Tokyo:20250419T170000 | |
DTEND;TZID=Asia/Tokyo:20250419T180000 | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_RRULE_UNTIL_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
DTSTART;TZID=Asia/Tokyo:20250701T090000 | |
DTEND;TZID=Asia/Tokyo:20250701T170000 | |
RRULE:FREQ=DAILY;UNTIL=20250831T170000;BYMONTH=7,8 | |
EXDATE;TZID=Asia/Tokyo:20250814T090000 | |
EXDATE;TZID=Asia/Tokyo:20250815T090000 | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
_RRULE_COUNT_ICS = """\ | |
BEGIN:VCALENDAR | |
BEGIN:VEVENT | |
DTSTART;TZID=Asia/Tokyo:20250701T090000 | |
DTEND;TZID=Asia/Tokyo:20250701T170000 | |
RRULE:FREQ=DAILY;COUNT=90 | |
EXDATE;TZID=Asia/Tokyo:20250814T090000 | |
EXDATE;TZID=Asia/Tokyo:20250815T090000 | |
END:VEVENT | |
END:VCALENDAR | |
""" | |
@pytest.mark.parametrize( | |
("ics_str", "now", "expected"), | |
[ | |
("", "2025-04-19 09:30:00+09:00", False), | |
("invalid-ics", "2025-04-19 09:30:00+09:00", False), | |
(_MEETING_ICS, "2025-04-19 09:30:00+09:00", True), | |
(_MEETING_ICS, "2025-04-19 08:59:00+09:00", False), | |
(_MEETING_ICS, "2025-04-19 10:01:00+09:00", False), | |
(_ALL_DAY_ICS, "2025-04-19 12:00:00+09:00", True), | |
(_ALL_DAY_ICS, "2025-04-21 00:00:00+09:00", False), | |
(_EMPTY_ICS, "2025-04-19 12:00:00+09:00", False), | |
(_DURATION_ICS, "2025-04-19 13:30:00+09:00", True), | |
(_DURATION_ICS, "2025-04-19 14:01:00+09:00", False), | |
(_NO_END_OR_DURATION_DATE_ICS, "2025-04-19 15:00:00+09:00", True), | |
(_NO_END_OR_DURATION_DATE_ICS, "2025-04-20 15:00:00+09:00", False), | |
(_NO_END_OR_DURATION_DATETIME_ICS, "2025-04-19 15:00:00+09:00", True), | |
(_NO_END_OR_DURATION_DATETIME_ICS, "2025-04-19 15:01:00+09:00", False), | |
(_NO_SUMMARY_ICS, "2025-04-19 17:30:00+09:00", True), | |
(_RRULE_UNTIL_ICS, "2025-07-01 10:00:00+09:00", True), | |
(_RRULE_UNTIL_ICS, "2025-07-01 18:00:00+09:00", False), | |
(_RRULE_UNTIL_ICS, "2025-08-15 10:00:00+09:00", False), | |
(_RRULE_UNTIL_ICS, "2025-08-15 18:00:00+09:00", False), | |
(_RRULE_UNTIL_ICS, "2025-08-31 10:00:00+09:00", True), | |
(_RRULE_UNTIL_ICS, "2025-08-31 18:00:00+09:00", False), | |
(_RRULE_COUNT_ICS, "2025-07-01 10:00:00+09:00", True), | |
(_RRULE_COUNT_ICS, "2025-07-01 18:00:00+09:00", False), | |
(_RRULE_COUNT_ICS, "2025-08-15 10:00:00+09:00", False), | |
(_RRULE_COUNT_ICS, "2025-08-15 18:00:00+09:00", False), | |
(_RRULE_COUNT_ICS, "2025-08-31 10:00:00+09:00", True), | |
(_RRULE_COUNT_ICS, "2025-08-31 18:00:00+09:00", False), | |
], | |
) | |
def test_check_if_now_in_event(ics_str: str, now: str, expected: bool) -> None: | |
with freeze_time(now): | |
assert check_if_now_in_event(ics_str) is expected |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment