Skip to content

Instantly share code, notes, and snippets.

@dceoy
Last active April 23, 2025 16:11
Show Gist options
  • Save dceoy/7e29ea15a3cbc494bbb40cc4f256dd7e to your computer and use it in GitHub Desktop.
Save dceoy/7e29ea15a3cbc494bbb40cc4f256dd7e to your computer and use it in GitHub Desktop.
[Python] ICS string checker
"""Utilities for working with iCalendar (ICS) data."""
import logging
import os
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from icalendar import Calendar
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def check_if_now_in_event(ics: str, timezone: str | None = None) -> 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.
Returns:
bool: Whether the current time is within any VEVENT in the iCalendar string.
"""
tz = timezone or os.getenv("TZ", "UTC")
now = datetime.now(ZoneInfo(tz))
logger.info("now: %s", now)
for v in Calendar.from_ical(ics).walk("VEVENT"):
input_start = v.decoded("DTSTART")
logger.info("input_start: %s", input_start)
if "DTEND" in v:
input_end = v.decoded("DTEND")
elif "DURATION" in v:
input_end = input_start + v.decoded("DURATION")
elif not isinstance(input_start, datetime):
input_end = input_start + timedelta(days=1)
else:
continue
logger.info("input_end: %s", input_end)
dt_start_end = [
(
datetime.combine(d, datetime.min.time())
if not isinstance(d, datetime)
else d
)
for d in [input_start, input_end]
]
logger.info("dt_start_end: %s", dt_start_end)
tz_dt_start_end = [
(d.replace(tzinfo=ZoneInfo(tz)) if d.tzinfo is None else d)
for d in dt_start_end
]
logger.info("tz_dt_start_end: %s", tz_dt_start_end)
if tz_dt_start_end[0] <= now < tz_dt_start_end[1]:
summary = v.decoded("SUMMARY") if "SUMMARY" in v else None
logger.info("Now in event: %s %s", summary, tuple(tz_dt_start_end))
return True
logger.info("Not in any event")
return False
"""Unit tests for ics.check_if_now_in_event."""
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
"""
@pytest.fixture
def mock_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("TZ", "Asia/Tokyo")
@pytest.mark.parametrize(
("ics_str", "now", "expected"),
[
(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", False),
(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),
],
)
def test_check_if_now_in_event(
ics_str: str, now: str, expected: bool, mock_env: None
) -> None:
with freeze_time(now):
assert check_if_now_in_event(ics_str) is expected
@pytest.mark.parametrize(
("ics_str", "now", "timezone", "expected"),
[
(MEETING_ICS, "2025-04-19 00:30:00+00:00", "UTC", True),
(MEETING_ICS, "2025-04-19 01:30:00+01:00", "Europe/London", True),
],
)
def test_check_if_now_in_event_with_timezone(
ics_str: str, now: str, timezone: str, expected: str, mock_env: None
) -> None:
with freeze_time(now):
assert check_if_now_in_event(ics_str, timezone=timezone) is expected
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment