Skip to content

Commit 8a869d0

Browse files
refactor: improve calendar event handling and date management
- Updated `generate_ics.py` to enhance class-off date handling and event creation. - Refactored `google_calendar.py` to streamline calendar creation and event addition. - Improved date generation logic in `dates.py` for better holiday and class-off date management. - Enhanced timezone handling in `build_event.py` for accurate datetime representation.
1 parent 3c52d0a commit 8a869d0

File tree

5 files changed

+223
-234
lines changed

5 files changed

+223
-234
lines changed

gyft.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,12 @@ def main():
7777
)
7878
else:
7979
_, sso_token = erp.login(headers, session)
80+
# sso_token = 'xxxx' # for testing purposes
81+
82+
# print(sso_token) # for testing purposes
8083

8184
roll_number = erp.ROLL_NUMBER
85+
# roll_number = 'xxxx' # for testing purposes
8286

8387
courses = get_courses(session, sso_token, roll_number)
8488

timetable/generate_ics.py

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from __future__ import print_function
22

33
from icalendar import Calendar, Event
4-
from datetime import datetime, timedelta
4+
from datetime import timedelta
55
from timetable import Course
6-
from utils import academic_calander_handler, dates, build_event_duration, generate_india_time, next_weekday
7-
8-
WORKING_DAYS = dates.get_dates()
6+
from utils import academic_calander_handler, dates, build_event_duration, generate_india_time
97

108

119
def generate_ics(courses: list[Course], output_filename, is_web=False):
@@ -14,55 +12,73 @@ def generate_ics(courses: list[Course], output_filename, is_web=False):
1412
"""
1513
cal = Calendar()
1614
cal.add("prodid", "-//Your Timetable generated by GYFT//mxm.dk//")
17-
cal.add("version", "1.0")
15+
cal.add("version", "2.0")
16+
17+
# Build exhaustive list of class-off dates for the semester
18+
class_off_days = dates.get_class_off_dates_in_semester()
19+
20+
# Helper: find first occurrence of course.day on/after SEM_BEGIN
21+
def first_occurrence_of_day(day_name: str):
22+
return dates.next_weekday(dates.SEM_BEGIN, day_name)
23+
24+
# For each course, create a single weekly recurring VEVENT with EXDATEs on class-off days
1825
for course in courses:
19-
start_dates = []
20-
end_dates = []
21-
for x in WORKING_DAYS:
22-
if next_weekday(x[0], course.day) <= x[1]:
23-
start_dates.append(
24-
next_weekday(x[0], course.day)
25-
) # work only in the interval 'x' of WORKING_DAYS, avoiding recurring outside of it.
26-
end_dates.append(x[1])
27-
lecture_begins_stamps = [
28-
generate_india_time(
29-
start.year, start.month, start.day, course.start_time, 0
30-
)
31-
for start in start_dates
32-
]
26+
first_day = first_occurrence_of_day(course.day)
27+
lecture_begin = generate_india_time(
28+
first_day.year, first_day.month, first_day.day, course.start_time, 0
29+
)
30+
31+
# Recurrence end: last occurrence should not exceed END_TERM_BEGIN
32+
until = dates.END_TERM_BEGIN
3333

34-
for lecture_begin, end in zip(lecture_begins_stamps, end_dates):
35-
event = build_event_duration(
36-
course.title,
37-
course.code,
38-
lecture_begin,
39-
course.duration,
40-
course.get_location(),
41-
"weekly",
42-
end,
43-
)
34+
event = build_event_duration(
35+
course.title,
36+
course.code,
37+
lecture_begin,
38+
course.duration,
39+
course.get_location(),
40+
"weekly",
41+
until,
42+
)
4443

45-
cal.add_component(event)
44+
# Compute EXDATE list: class-off days that fall on the course weekday
45+
# Build exclusion timestamps at the same local time as lecture_begin
46+
exdates = []
47+
for off_day in class_off_days:
48+
if off_day.weekday() == first_day.weekday():
49+
ex_dt = generate_india_time(
50+
off_day.year, off_day.month, off_day.day, course.start_time, 0
51+
)
52+
# Only exclude occurrences within [lecture_begin, until)
53+
if lecture_begin <= ex_dt < until:
54+
exdates.append(ex_dt)
4655

47-
# add holidays
56+
if exdates:
57+
# icalendar supports adding list of datetimes as EXDATE
58+
event.add("exdate", exdates)
59+
60+
cal.add_component(event)
61+
62+
# add holidays (as all-day events in Asia/Kolkata)
4863
for holiday in dates.holidays:
4964
event = Event()
5065
event.add("summary", "INSTITUTE HOLIDAY : " + holiday[0])
51-
event.add("dtstart", holiday[1])
52-
event.add("dtend", holiday[1] + timedelta(days=1))
66+
hdt = holiday[1]
67+
hend = hdt + timedelta(days=1)
68+
event.add("dtstart", hdt)
69+
event.add("dtend", hend)
5370
cal.add_component(event)
5471

5572
for entry in academic_calander_handler.get_academic_calendar(is_web):
5673
event = Event()
5774
event.add("summary", entry.event)
58-
event.add("dtstart",entry.start_date)
59-
event.add("dtend",entry.end_date)
75+
event.add("dtstart", entry.start_date)
76+
event.add("dtend", entry.end_date)
6077
cal.add_component(event)
61-
6278

63-
if output_filename != "":
79+
if output_filename:
6480
with open(output_filename, "wb") as f:
6581
f.write(cal.to_ical())
6682
print("\nYour timetable has been written to %s" % output_filename)
67-
68-
return cal.to_ical().decode('utf-8')
83+
84+
return cal.to_ical()

timetable/google_calendar.py

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
from utils import (
1313
END_TERM_BEGIN,
1414
SEM_BEGIN,
15-
GYFT_RECUR_STRS,
16-
get_rfc_time,
17-
hdays,
1815
holidays,
16+
dates,
17+
generate_india_time,
1918
)
19+
from utils import academic_calander_handler
2020
from timetable import Course
2121

2222
DEBUG = False
@@ -53,7 +53,7 @@ def get_credentials() -> client.Credentials:
5353
return credentials
5454

5555

56-
def create_calendar(courses: list[Course]) -> None:
56+
def create_calendar(courses: list[Course], is_web: bool = False) -> None:
5757
r"""
5858
Adds courses to Google Calendar
5959
Args:
@@ -62,77 +62,103 @@ def create_calendar(courses: list[Course]) -> None:
6262
credentials = get_credentials()
6363
http = credentials.authorize(httplib2.Http())
6464
service = discovery.build("calendar", "v3", http=http)
65+
66+
# Helper: get or create the dedicated 'IIT KGP' calendar
67+
def get_or_create_calendar_id(name: str = "IIT KGP", tz: str = "Asia/Kolkata") -> str:
68+
page_token = None
69+
while True:
70+
cl = service.calendarList().list(pageToken=page_token, maxResults=250).execute()
71+
for item in cl.get("items", []):
72+
if item.get("summary") == name and item.get("accessRole") in ("owner", "writer"):
73+
return item["id"]
74+
page_token = cl.get("nextPageToken")
75+
if not page_token:
76+
break
77+
# Create if not found
78+
body = {"summary": name, "timeZone": tz}
79+
created = service.calendars().insert(body=body).execute()
80+
return created["id"]
81+
82+
calendar_id = get_or_create_calendar_id()
6583
batch = service.new_batch_http_request() # To add events in a batch
84+
85+
# Build exhaustive class-off dates once
86+
class_off_days = dates.get_class_off_dates_in_semester()
87+
88+
# Helper: first occurrence of the course weekday on/after SEM_BEGIN
89+
def first_occurrence_of_day(day_name: str):
90+
return dates.next_weekday(SEM_BEGIN, day_name)
91+
6692
for course in courses:
93+
first_day = first_occurrence_of_day(course.day)
94+
lecture_begin_dt = generate_india_time(
95+
first_day.year, first_day.month, first_day.day, course.start_time, 0
96+
)
97+
lecture_end_dt = lecture_begin_dt + timedelta(hours=course.duration)
98+
99+
# Build EXDATEs at the lecture start time for class-off days matching the course weekday
100+
exdate_stamps: list[str] = []
101+
for off_day in class_off_days:
102+
if off_day.weekday() == first_day.weekday():
103+
ex_dt = generate_india_time(
104+
off_day.year, off_day.month, off_day.day, course.start_time, 0
105+
)
106+
if lecture_begin_dt <= ex_dt < END_TERM_BEGIN:
107+
exdate_stamps.append(ex_dt.strftime("%Y%m%dT%H%M%S"))
108+
67109
event = {
68110
"summary": course.title,
69111
"location": course.get_location(),
112+
"description": course.code,
70113
"start": {
71-
"dateTime": get_rfc_time(course.start_time, course.day)[:-7],
114+
"dateTime": lecture_begin_dt.strftime("%Y-%m-%dT%H:%M:%S"),
72115
"timeZone": "Asia/Kolkata",
73116
},
74117
"end": {
75-
"dateTime": get_rfc_time(course.end_time, course.day)[:-7],
118+
"dateTime": lecture_end_dt.strftime("%Y-%m-%dT%H:%M:%S"),
76119
"timeZone": "Asia/Kolkata",
77120
},
78-
}
79-
80-
### making a string to pass in exdate. Changed the time of the string to class start time
81-
exdate_str_dict = defaultdict(str)
82-
83-
for day in hdays[course.day]:
84-
exdate_str_dict[course.day] += (
85-
day.replace(hour=course.start_time).strftime("%Y%m%dT%H%M%S") + ","
86-
)
87-
if exdate_str_dict[course.day] != None:
88-
exdate_str_dict[course.day] = exdate_str_dict[course.day][:-1]
89-
90-
if (
91-
exdate_str_dict[course.day] != None
92-
): ## if holiday exists on this recurrence, skip it with exdate
93-
event["recurrence"] = [
94-
"EXDATE;TZID=Asia/Kolkata:{}".format(exdate_str_dict[course.day]),
121+
"recurrence": [
122+
# Add EXDATE first if any
123+
*( ["EXDATE;TZID=Asia/Kolkata:" + ",".join(exdate_stamps)] if exdate_stamps else [] ),
95124
"RRULE:FREQ=WEEKLY;UNTIL={}".format(
96125
END_TERM_BEGIN.strftime("%Y%m%dT000000Z")
97126
),
98-
]
99-
100-
else:
101-
event["recurrence"] = [
102-
"RRULE:FREQ=WEEKLY;UNTIL={}".format(
103-
END_TERM_BEGIN.strftime("%Y%m%dT000000Z")
104-
)
105-
]
127+
],
128+
}
106129

107-
batch.add(service.events().insert(calendarId="primary", body=event))
130+
batch.add(service.events().insert(calendarId=calendar_id, body=event))
108131
print("Added " + event["summary"])
109-
110132
batch.execute() ## execute batch of timetable
111133

112-
# add holidays to calender as events
134+
# add holidays to calendar as all-day events (Asia/Kolkata midnight)
113135
for holiday in holidays:
114-
if (
115-
holiday[1].date() >= date.today()
116-
and holiday[1].date() <= END_TERM_BEGIN.date()
117-
):
118-
holiday_event = {
119-
"summary": "INSTITUTE HOLIDAY : " + holiday[0],
120-
"start": {
121-
"dateTime": holiday[1].strftime("%Y-%m-%dT00:00:00"),
122-
"timeZone": "Asia/Kolkata",
123-
},
124-
"end": {
125-
"dateTime": (holiday[1] + timedelta(days=1)).strftime(
126-
"%Y-%m-%dT00:00:00"
127-
),
128-
"timeZone": "Asia/Kolkata",
129-
},
130-
}
131-
insert = (
132-
service.events()
133-
.insert(calendarId="primary", body=holiday_event)
134-
.execute()
135-
)
136+
hdt = holiday[1]
137+
start_str = generate_india_time(hdt.year, hdt.month, hdt.day, 0, 0).strftime("%Y-%m-%dT00:00:00")
138+
end_dt = generate_india_time(hdt.year, hdt.month, hdt.day, 0, 0) + timedelta(days=1)
139+
holiday_event = {
140+
"summary": "INSTITUTE HOLIDAY : " + holiday[0],
141+
"start": {
142+
"dateTime": start_str,
143+
"timeZone": "Asia/Kolkata",
144+
},
145+
"end": {
146+
"dateTime": end_dt.strftime("%Y-%m-%dT00:00:00"),
147+
"timeZone": "Asia/Kolkata",
148+
},
149+
}
150+
service.events().insert(calendarId=calendar_id, body=holiday_event).execute()
151+
152+
# add academic calendar entries as all-day events
153+
for entry in academic_calander_handler.get_academic_calendar(is_web):
154+
start_str = entry.start_date.strftime("%Y-%m-%dT00:00:00")
155+
end_str = entry.end_date.strftime("%Y-%m-%dT00:00:00")
156+
event = {
157+
"summary": entry.event,
158+
"start": {"dateTime": start_str, "timeZone": "Asia/Kolkata"},
159+
"end": {"dateTime": end_str, "timeZone": "Asia/Kolkata"},
160+
}
161+
service.events().insert(calendarId=calendar_id, body=event).execute()
136162

137163
print("\nAll events added successfully!\n")
138164

@@ -144,28 +170,24 @@ def delete_calendar():
144170
credentials = get_credentials()
145171
http = credentials.authorize(httplib2.Http())
146172
service = discovery.build("calendar", "v3", http=http)
147-
batch = service.new_batch_http_request() # To add events in a batch
148-
print("Getting the events")
149-
events_result = (
150-
service.events()
151-
.list(
152-
calendarId="primary",
153-
timeMin=SEM_BEGIN.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
154-
singleEvents=False,
155-
timeMax=END_TERM_BEGIN.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
156-
maxResults=2500,
157-
)
158-
.execute()
159-
)
160-
events = events_result.get("items", [])
161-
if not events or len(events) == 0:
162-
print("No upcoming events found.")
173+
174+
# Find the IIT KGP calendar and delete it entirely
175+
target_name = "IIT KGP"
176+
page_token = None
177+
calendar_id = None
178+
while True:
179+
cl = service.calendarList().list(pageToken=page_token, maxResults=250).execute()
180+
for item in cl.get("items", []):
181+
if item.get("summary") == target_name and item.get("accessRole") in ("owner", "writer"):
182+
calendar_id = item["id"]
183+
break
184+
if calendar_id or not cl.get("nextPageToken"):
185+
break
186+
page_token = cl.get("nextPageToken")
187+
188+
if not calendar_id:
189+
print(f"Calendar '{target_name}' not found. Nothing to delete.")
163190
return
164-
for event in events:
165-
if event.get("recurrence", "NoRecur") in GYFT_RECUR_STRS:
166-
batch.add(
167-
service.events().delete(calendarId="primary", eventId=event["id"])
168-
)
169-
print("Deleted: ", event["summary"], event["start"])
170-
batch.execute()
171-
print("Deletion done!")
191+
192+
service.calendars().delete(calendarId=calendar_id).execute()
193+
print(f"Calendar '{target_name}' deleted.")

utils/build_event.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,10 @@ def build_event_duration(summary: str, description: str, start: datetime, durati
3434

3535
# Converts time to datetime object for India/Asia/Kolkata timezone
3636
def generate_india_time(year: int, month: int, date: int, hour: int, minutes: int):
37-
return datetime(year, month, date, hour, minutes, tzinfo=pytz.timezone('Asia/Kolkata'))
37+
"""
38+
Create a timezone-aware datetime in Asia/Kolkata.
39+
Note: Use tz.localize to avoid incorrect LMT offsets from assigning tzinfo directly.
40+
"""
41+
tz = pytz.timezone('Asia/Kolkata')
42+
naive = datetime(year, month, date, hour, minutes)
43+
return tz.localize(naive)

0 commit comments

Comments
 (0)