Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gyft.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ def main():
)
else:
_, sso_token = erp.login(headers, session)
# sso_token = 'xxxx' # for testing purposes

# print(sso_token) # for testing purposes

roll_number = erp.ROLL_NUMBER
# roll_number = 'xxxx' # for testing purposes

courses = get_courses(session, sso_token, roll_number)

Expand Down
94 changes: 55 additions & 39 deletions timetable/generate_ics.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from __future__ import print_function

from icalendar import Calendar, Event
from datetime import datetime, timedelta
from datetime import timedelta
from timetable import Course
from utils import academic_calander_handler, dates, build_event_duration, generate_india_time, next_weekday

WORKING_DAYS = dates.get_dates()
from utils import academic_calander_handler, dates, build_event_duration, generate_india_time


def generate_ics(courses: list[Course], output_filename, is_web=False):
Expand All @@ -14,55 +12,73 @@ def generate_ics(courses: list[Course], output_filename, is_web=False):
"""
cal = Calendar()
cal.add("prodid", "-//Your Timetable generated by GYFT//mxm.dk//")
cal.add("version", "1.0")
cal.add("version", "2.0")

# Build exhaustive list of class-off dates for the semester
class_off_days = dates.get_class_off_dates_in_semester()

# Helper: find first occurrence of course.day on/after SEM_BEGIN
def first_occurrence_of_day(day_name: str):
return dates.next_weekday(dates.SEM_BEGIN, day_name)

# For each course, create a single weekly recurring VEVENT with EXDATEs on class-off days
for course in courses:
start_dates = []
end_dates = []
for x in WORKING_DAYS:
if next_weekday(x[0], course.day) <= x[1]:
start_dates.append(
next_weekday(x[0], course.day)
) # work only in the interval 'x' of WORKING_DAYS, avoiding recurring outside of it.
end_dates.append(x[1])
lecture_begins_stamps = [
generate_india_time(
start.year, start.month, start.day, course.start_time, 0
)
for start in start_dates
]
first_day = first_occurrence_of_day(course.day)
lecture_begin = generate_india_time(
first_day.year, first_day.month, first_day.day, course.start_time, 0
)

# Recurrence end: last occurrence should not exceed END_TERM_BEGIN
until = dates.END_TERM_BEGIN

for lecture_begin, end in zip(lecture_begins_stamps, end_dates):
event = build_event_duration(
course.title,
course.code,
lecture_begin,
course.duration,
course.get_location(),
"weekly",
end,
)
event = build_event_duration(
course.title,
course.code,
lecture_begin,
course.duration,
course.get_location(),
"weekly",
until,
)

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

# add holidays
if exdates:
# icalendar supports adding list of datetimes as EXDATE
event.add("exdate", exdates)

cal.add_component(event)

# add holidays (as all-day events in Asia/Kolkata)
for holiday in dates.holidays:
event = Event()
event.add("summary", "INSTITUTE HOLIDAY : " + holiday[0])
event.add("dtstart", holiday[1])
event.add("dtend", holiday[1] + timedelta(days=1))
hdt = holiday[1]
hend = hdt + timedelta(days=1)
event.add("dtstart", hdt)
event.add("dtend", hend)
cal.add_component(event)

for entry in academic_calander_handler.get_academic_calendar(is_web):
event = Event()
event.add("summary", entry.event)
event.add("dtstart",entry.start_date)
event.add("dtend",entry.end_date)
event.add("dtstart", entry.start_date)
event.add("dtend", entry.end_date)
cal.add_component(event)


if output_filename != "":
if output_filename:
with open(output_filename, "wb") as f:
f.write(cal.to_ical())
print("\nYour timetable has been written to %s" % output_filename)
return cal.to_ical().decode('utf-8')

return cal.to_ical()
182 changes: 102 additions & 80 deletions timetable/google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
from utils import (
END_TERM_BEGIN,
SEM_BEGIN,
GYFT_RECUR_STRS,
get_rfc_time,
hdays,
holidays,
dates,
generate_india_time,
)
from utils import academic_calander_handler
from timetable import Course

DEBUG = False
Expand Down Expand Up @@ -53,7 +53,7 @@ def get_credentials() -> client.Credentials:
return credentials


def create_calendar(courses: list[Course]) -> None:
def create_calendar(courses: list[Course], is_web: bool = False) -> None:
r"""
Adds courses to Google Calendar
Args:
Expand All @@ -62,77 +62,103 @@ def create_calendar(courses: list[Course]) -> None:
credentials = get_credentials()
http = credentials.authorize(httplib2.Http())
service = discovery.build("calendar", "v3", http=http)

# Helper: get or create the dedicated 'IIT KGP' calendar
def get_or_create_calendar_id(name: str = "IIT KGP", tz: str = "Asia/Kolkata") -> str:
page_token = None
while True:
cl = service.calendarList().list(pageToken=page_token, maxResults=250).execute()
for item in cl.get("items", []):
if item.get("summary") == name and item.get("accessRole") in ("owner", "writer"):
return item["id"]
page_token = cl.get("nextPageToken")
if not page_token:
break
# Create if not found
body = {"summary": name, "timeZone": tz}
created = service.calendars().insert(body=body).execute()
return created["id"]

calendar_id = get_or_create_calendar_id()
batch = service.new_batch_http_request() # To add events in a batch

# Build exhaustive class-off dates once
class_off_days = dates.get_class_off_dates_in_semester()

# Helper: first occurrence of the course weekday on/after SEM_BEGIN
def first_occurrence_of_day(day_name: str):
return dates.next_weekday(SEM_BEGIN, day_name)

for course in courses:
first_day = first_occurrence_of_day(course.day)
lecture_begin_dt = generate_india_time(
first_day.year, first_day.month, first_day.day, course.start_time, 0
)
lecture_end_dt = lecture_begin_dt + timedelta(hours=course.duration)

# Build EXDATEs at the lecture start time for class-off days matching the course weekday
exdate_stamps: list[str] = []
for off_day in class_off_days:
if off_day.weekday() == first_day.weekday():
ex_dt = generate_india_time(
off_day.year, off_day.month, off_day.day, course.start_time, 0
)
if lecture_begin_dt <= ex_dt < END_TERM_BEGIN:
exdate_stamps.append(ex_dt.strftime("%Y%m%dT%H%M%S"))

event = {
"summary": course.title,
"location": course.get_location(),
"description": course.code,
"start": {
"dateTime": get_rfc_time(course.start_time, course.day)[:-7],
"dateTime": lecture_begin_dt.strftime("%Y-%m-%dT%H:%M:%S"),
"timeZone": "Asia/Kolkata",
},
"end": {
"dateTime": get_rfc_time(course.end_time, course.day)[:-7],
"dateTime": lecture_end_dt.strftime("%Y-%m-%dT%H:%M:%S"),
"timeZone": "Asia/Kolkata",
},
}

### making a string to pass in exdate. Changed the time of the string to class start time
exdate_str_dict = defaultdict(str)

for day in hdays[course.day]:
exdate_str_dict[course.day] += (
day.replace(hour=course.start_time).strftime("%Y%m%dT%H%M%S") + ","
)
if exdate_str_dict[course.day] != None:
exdate_str_dict[course.day] = exdate_str_dict[course.day][:-1]

if (
exdate_str_dict[course.day] != None
): ## if holiday exists on this recurrence, skip it with exdate
event["recurrence"] = [
"EXDATE;TZID=Asia/Kolkata:{}".format(exdate_str_dict[course.day]),
"recurrence": [
# Add EXDATE first if any
*( ["EXDATE;TZID=Asia/Kolkata:" + ",".join(exdate_stamps)] if exdate_stamps else [] ),
"RRULE:FREQ=WEEKLY;UNTIL={}".format(
END_TERM_BEGIN.strftime("%Y%m%dT000000Z")
),
]

else:
event["recurrence"] = [
"RRULE:FREQ=WEEKLY;UNTIL={}".format(
END_TERM_BEGIN.strftime("%Y%m%dT000000Z")
)
]
],
}

batch.add(service.events().insert(calendarId="primary", body=event))
batch.add(service.events().insert(calendarId=calendar_id, body=event))
print("Added " + event["summary"])

batch.execute() ## execute batch of timetable

# add holidays to calender as events
# add holidays to calendar as all-day events (Asia/Kolkata midnight)
for holiday in holidays:
if (
holiday[1].date() >= date.today()
and holiday[1].date() <= END_TERM_BEGIN.date()
):
holiday_event = {
"summary": "INSTITUTE HOLIDAY : " + holiday[0],
"start": {
"dateTime": holiday[1].strftime("%Y-%m-%dT00:00:00"),
"timeZone": "Asia/Kolkata",
},
"end": {
"dateTime": (holiday[1] + timedelta(days=1)).strftime(
"%Y-%m-%dT00:00:00"
),
"timeZone": "Asia/Kolkata",
},
}
insert = (
service.events()
.insert(calendarId="primary", body=holiday_event)
.execute()
)
hdt = holiday[1]
start_str = generate_india_time(hdt.year, hdt.month, hdt.day, 0, 0).strftime("%Y-%m-%dT00:00:00")
end_dt = generate_india_time(hdt.year, hdt.month, hdt.day, 0, 0) + timedelta(days=1)
holiday_event = {
"summary": "INSTITUTE HOLIDAY : " + holiday[0],
"start": {
"dateTime": start_str,
"timeZone": "Asia/Kolkata",
},
"end": {
"dateTime": end_dt.strftime("%Y-%m-%dT00:00:00"),
"timeZone": "Asia/Kolkata",
},
}
service.events().insert(calendarId=calendar_id, body=holiday_event).execute()

# add academic calendar entries as all-day events
for entry in academic_calander_handler.get_academic_calendar(is_web):
start_str = entry.start_date.strftime("%Y-%m-%dT00:00:00")
end_str = entry.end_date.strftime("%Y-%m-%dT00:00:00")
event = {
"summary": entry.event,
"start": {"dateTime": start_str, "timeZone": "Asia/Kolkata"},
"end": {"dateTime": end_str, "timeZone": "Asia/Kolkata"},
}
service.events().insert(calendarId=calendar_id, body=event).execute()

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

Expand All @@ -144,28 +170,24 @@ def delete_calendar():
credentials = get_credentials()
http = credentials.authorize(httplib2.Http())
service = discovery.build("calendar", "v3", http=http)
batch = service.new_batch_http_request() # To add events in a batch
print("Getting the events")
events_result = (
service.events()
.list(
calendarId="primary",
timeMin=SEM_BEGIN.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
singleEvents=False,
timeMax=END_TERM_BEGIN.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
maxResults=2500,
)
.execute()
)
events = events_result.get("items", [])
if not events or len(events) == 0:
print("No upcoming events found.")

# Find the IIT KGP calendar and delete it entirely
target_name = "IIT KGP"
page_token = None
calendar_id = None
while True:
cl = service.calendarList().list(pageToken=page_token, maxResults=250).execute()
for item in cl.get("items", []):
if item.get("summary") == target_name and item.get("accessRole") in ("owner", "writer"):
calendar_id = item["id"]
break
if calendar_id or not cl.get("nextPageToken"):
break
page_token = cl.get("nextPageToken")

if not calendar_id:
print(f"Calendar '{target_name}' not found. Nothing to delete.")
return
for event in events:
if event.get("recurrence", "NoRecur") in GYFT_RECUR_STRS:
batch.add(
service.events().delete(calendarId="primary", eventId=event["id"])
)
print("Deleted: ", event["summary"], event["start"])
batch.execute()
print("Deletion done!")

service.calendars().delete(calendarId=calendar_id).execute()
print(f"Calendar '{target_name}' deleted.")
8 changes: 7 additions & 1 deletion utils/build_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ def build_event_duration(summary: str, description: str, start: datetime, durati

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