From 02f2621539ddd1c77702312296fa9af0cc7d5445 Mon Sep 17 00:00:00 2001 From: gwydion67 Date: Sat, 19 Oct 2024 11:36:51 +0530 Subject: [PATCH 1/8] update gitignore --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 00bf393..8c25f89 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,9 @@ data.txt .idea/ .vscode venv -.env \ No newline at end of file +.env + +ACADEMIC_CALENDAR_*.pdf +Academic_Cal-j/** +final.json + From fcd45efdc4dde19163eb88c0ed63bef43168724b Mon Sep 17 00:00:00 2001 From: gwydion67 Date: Thu, 24 Oct 2024 06:40:29 +0530 Subject: [PATCH 2/8] rewrite google calendar to use the ICS file to generate the google calendar entries --- .gitignore | 2 +- gyft.py | 8 +- timetable/google_calendar.py | 216 ++++++++++++++++++++++------------- 3 files changed, 144 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 8c25f89..6661281 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ venv ACADEMIC_CALENDAR_*.pdf Academic_Cal-j/** final.json - +erpcreds.py diff --git a/gyft.py b/gyft.py index 7a20d86..bd832c9 100644 --- a/gyft.py +++ b/gyft.py @@ -22,6 +22,10 @@ def parse_args(): action="store_true", help="Delete events automatically added by the script before adding new events", ) + + parser.add_argument('-c', "--cal", dest='cal', default="primary", + help="Name of the calendar in which the events will be added to, you will have to manually create a calendar on the google calendar app/website",) + args = parser.parse_args() return args @@ -53,8 +57,10 @@ def main(): print("3. Exit") choice = int(input("Enter your choice: ")) + print(args.cal) + if choice == 1: - create_calendar(courses) + create_calendar(courses,args.cal) elif choice == 2: generate_ics(courses, output_filename) else: diff --git a/timetable/google_calendar.py b/timetable/google_calendar.py index 343991f..7b00471 100644 --- a/timetable/google_calendar.py +++ b/timetable/google_calendar.py @@ -1,21 +1,18 @@ from __future__ import print_function import os +from bs4 import BeautifulSoup +from googleapiclient.discovery import re import httplib2 from apiclient import discovery +import icalendar from oauth2client import client, file, tools -from oauth2client import file -from oauth2client import tools -from datetime import datetime, timedelta, date -from collections import defaultdict -from icalendar import Event +from icalendar import Calendar +from timetable.generate_ics import generate_ics from utils import ( END_TERM_BEGIN, SEM_BEGIN, GYFT_RECUR_STRS, - get_rfc_time, - hdays, - holidays, ) from timetable import Course @@ -25,6 +22,98 @@ CLIENT_SECRET_FILE = "client_secret.json" APPLICATION_NAME = "gyft" +def parse_ics(ics): + events = [] + with open(ics, 'r') as rf: + ical = Calendar().from_ical(rf.read()) + for i, comp in enumerate(ical.walk()): + if comp.name == "VEVENT": + event = {} + for name, prop in comp.property_items(): + + if name in ["SUMMARY", "LOCATION"]: + event[name.lower()] = prop.to_ical().decode('utf-8') + + elif name == "DTSTART": + event["start"] = { + "dateTime": prop.dt.isoformat(), + "timeZone": ( str(prop.dt.tzinfo) if prop.dt.tzinfo else "Asia/Kolkata" ) + } + + elif name == "DTEND": + event["end"] = { + "dateTime": prop.dt.isoformat(), + "timeZone": ( str(prop.dt.tzinfo) if prop.dt.tzinfo else "Asia/Kolkata" ) + } + elif name == "RRULE": + freq = str(prop.get("FREQ")[0]).strip() + duration = comp.get('duration').dt + end_time = (comp.get('dtstart').dt + duration) + until = prop.get('UNTIL')[0] + event["recurrence"] = [ + ("RRULE:FREQ="+freq+";UNTIL={}").format( + until.strftime("%Y%m%dT000000Z") + ) + ] + event["end"]= { + 'dateTime': end_time.isoformat(), + "timeZone": "Asia/Kolkata" + } + elif name == "SEQUENCE": + event[name.lower()] = prop + + elif name == "TRANSP": + event["transparency"] = prop.lower() + + elif name == "CLASS": + event["visibility"] = prop.lower() + + elif name == "ORGANIZER": + event["organizer"] = { + "displayName": prop.params.get("CN") or '', + "email": re.match('mailto:(.*)', prop).group(1) or '' + } + + elif name == "DESCRIPTION": + desc = prop.to_ical().decode('utf-8') + desc = desc.replace(u'\xa0', u' ') + if name.lower() in event: + event[name.lower()] = desc + '\r\n' + event[name.lower()] + else: + event[name.lower()] = desc + + elif name == 'X-ALT-DESC' and "description" not in event: + soup = BeautifulSoup(prop, 'lxml') + desc = soup.body.text.replace(u'\xa0', u' ') + if 'description' in event: + event['description'] += '\r\n' + desc + else: + event['description'] = desc + + elif name == 'ATTENDEE': + if 'attendees' not in event: + event["attendees"] = [] + RSVP = prop.params.get('RSVP') or '' + RSVP = 'RSVP={}'.format('TRUE:{}'.format(prop) if RSVP == 'TRUE' else RSVP) + ROLE = prop.params.get('ROLE') or '' + event['attendees'].append({ + 'displayName': prop.params.get('CN') or '', + 'email': re.match('mailto:(.*)', prop).group(1) or '', + 'comment': ROLE + # 'comment': '{};{}'.format(RSVP, ROLE) + }) + + # VALARM: only remind by UI popup + elif name == 'ACTION': + event['reminders'] = {'useDefault': True} + + else: + # print(name) + pass + + events.append(event) + + return events def get_credentials() -> client.Credentials: """Gets valid user credentials from storage. @@ -53,7 +142,29 @@ def get_credentials() -> client.Credentials: return credentials -def create_calendar(courses: list[Course]) -> None: +def get_calendarId(service, summary): + page_token = None + print("search ", summary , " calendar") + while True: + calendar_list = service.calendarList().list(pageToken=page_token).execute() + for calendar_list_entry in calendar_list['items']: + if calendar_list_entry['summary'] == summary: + return calendar_list_entry['id'] + page_token = calendar_list.get('nextPageToken') + if not page_token: + return "primary" + + + +def cb_insert_event(request_id, response, e): + summary = response['summary'] if response and 'summary' in response else '?' + if not e: + print('({}) - Insert event {}'.format(request_id, summary)) + else: + print('({}) - Exception {}'.format(request_id, e)) + + +def create_calendar(courses: list[Course], cal_name:str) -> None: r""" Adds courses to Google Calendar Args: @@ -62,77 +173,22 @@ def create_calendar(courses: list[Course]) -> None: 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 - for course in courses: - event = { - "summary": course.title, - "location": course.get_location(), - "start": { - "dateTime": get_rfc_time(course.start_time, course.day)[:-7], - "timeZone": "Asia/Kolkata", - }, - "end": { - "dateTime": get_rfc_time(course.end_time, course.day)[:-7], - "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]), - "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)) - print("Added " + event["summary"]) - - batch.execute() ## execute batch of timetable - - # add holidays to calender as events - 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() - ) + batch = service.new_batch_http_request(callback=cb_insert_event) # To add events in a batch + generate_ics(courses, "temp.ics") + + calendar_id = get_calendarId(service,cal_name) + print(calendar_id) + events = parse_ics("temp.ics") + + + for i, event in enumerate(events): + try: + print("[ADDING EVENT]: ",event,"\n") + batch.add(service.events().insert(calendarId=calendar_id, body=event)) + except Exception as e: + print(e) + batch.execute(http=http) + os.remove("temp.ics") print("\nAll events added successfully!\n") From 55df40e10913073ca9b02b8d374c331b01fbd7df Mon Sep 17 00:00:00 2001 From: gwydion67 Date: Thu, 24 Oct 2024 07:01:25 +0530 Subject: [PATCH 3/8] use already generated timetable if present (saving time) else generate a temp ics file --- timetable/google_calendar.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/timetable/google_calendar.py b/timetable/google_calendar.py index 7b00471..50caf28 100644 --- a/timetable/google_calendar.py +++ b/timetable/google_calendar.py @@ -174,11 +174,14 @@ def create_calendar(courses: list[Course], cal_name:str) -> None: http = credentials.authorize(httplib2.Http()) service = discovery.build("calendar", "v3", http=http) batch = service.new_batch_http_request(callback=cb_insert_event) # To add events in a batch - generate_ics(courses, "temp.ics") + filename = "timetable.ics" if os.path.exists("timetable.ics") else "temp.ics" + if(filename == "timetable.ics"): + print("Using existing timetable.ics, press 'n' to generate and use a temporary one") + if(input().lower() == "n"): + generate_ics(courses, "temp.ics") calendar_id = get_calendarId(service,cal_name) - print(calendar_id) - events = parse_ics("temp.ics") + events = parse_ics(filename) for i, event in enumerate(events): @@ -188,7 +191,8 @@ def create_calendar(courses: list[Course], cal_name:str) -> None: except Exception as e: print(e) batch.execute(http=http) - os.remove("temp.ics") + if(os.path.exists("temp.ics")): + os.remove("temp.ics") print("\nAll events added successfully!\n") From 49af17419f0324d853da111f2010f3b3a6a49cd1 Mon Sep 17 00:00:00 2001 From: gwydion67 Date: Thu, 24 Oct 2024 07:08:22 +0530 Subject: [PATCH 4/8] slightly improve the prompt for using already present timetable ics file --- timetable/google_calendar.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/timetable/google_calendar.py b/timetable/google_calendar.py index 50caf28..251c2a6 100644 --- a/timetable/google_calendar.py +++ b/timetable/google_calendar.py @@ -144,7 +144,6 @@ def get_credentials() -> client.Credentials: def get_calendarId(service, summary): page_token = None - print("search ", summary , " calendar") while True: calendar_list = service.calendarList().list(pageToken=page_token).execute() for calendar_list_entry in calendar_list['items']: @@ -176,8 +175,8 @@ def create_calendar(courses: list[Course], cal_name:str) -> None: batch = service.new_batch_http_request(callback=cb_insert_event) # To add events in a batch filename = "timetable.ics" if os.path.exists("timetable.ics") else "temp.ics" if(filename == "timetable.ics"): - print("Using existing timetable.ics, press 'n' to generate and use a temporary one") - if(input().lower() == "n"): + print("Using existing timetable.ics file, press 'n' to generate and use a temporary one or 'y' to continue : (Y/n)") + if(input().lower() == "n" and True): generate_ics(courses, "temp.ics") calendar_id = get_calendarId(service,cal_name) From bb8675231f367c0d889b746c66932db110f0f75c Mon Sep 17 00:00:00 2001 From: gwydion67 Date: Thu, 24 Oct 2024 08:22:53 +0530 Subject: [PATCH 5/8] Add option during Google Calendar Generation to add only current semester calendar or full semester --- timetable/google_calendar.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/timetable/google_calendar.py b/timetable/google_calendar.py index 251c2a6..79395cf 100644 --- a/timetable/google_calendar.py +++ b/timetable/google_calendar.py @@ -1,4 +1,5 @@ from __future__ import print_function +from datetime import datetime import os from bs4 import BeautifulSoup from googleapiclient.discovery import re @@ -15,6 +16,7 @@ GYFT_RECUR_STRS, ) from timetable import Course +from utils.dates import MID_TERM_END DEBUG = False @@ -22,12 +24,20 @@ CLIENT_SECRET_FILE = "client_secret.json" APPLICATION_NAME = "gyft" -def parse_ics(ics): +def is_in_correct_sem(dt: datetime) -> bool: + if(datetime.now().replace(tzinfo=None) <= MID_TERM_END.replace(tzinfo=None)): + return dt.replace(tzinfo=None) < MID_TERM_END.replace(tzinfo=None) + elif( datetime.now().replace(tzinfo=None) >= MID_TERM_END.replace(tzinfo=None)): + return dt.replace(tzinfo=None) < END_TERM_BEGIN.replace(tzinfo=None) and dt.replace(tzinfo=None) > MID_TERM_END.replace(tzinfo=None) + else: + return True + +def parse_ics(ics,length): events = [] with open(ics, 'r') as rf: ical = Calendar().from_ical(rf.read()) for i, comp in enumerate(ical.walk()): - if comp.name == "VEVENT": + if ((comp.name == "VEVENT") and ( length == "c" and is_in_correct_sem(comp.get('dtstart').dt)) ) : event = {} for name, prop in comp.property_items(): @@ -178,10 +188,15 @@ def create_calendar(courses: list[Course], cal_name:str) -> None: print("Using existing timetable.ics file, press 'n' to generate and use a temporary one or 'y' to continue : (Y/n)") if(input().lower() == "n" and True): generate_ics(courses, "temp.ics") + else: + print("Invalid input") + exit(1) calendar_id = get_calendarId(service,cal_name) - events = parse_ics(filename) - + length = input("Do you want Events from (C)urrent or (B)oth Semesters (C/b) _default is current_ : ") or 'c' + if(length.lower() == 'b'): + print("WARNING: Events from both semesters will be added.\n This may result in duplicate events if tool is used in both semesters") + events = parse_ics(filename, length.lower()) for i, event in enumerate(events): try: From 4ceb00e0db9e43105c95d2393f713d60bc9a0730 Mon Sep 17 00:00:00 2001 From: proffapt Date: Tue, 4 Feb 2025 22:53:14 +0530 Subject: [PATCH 6/8] feat: cli ci test --- .github/workflows/cli-ci-test.yaml | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/cli-ci-test.yaml diff --git a/.github/workflows/cli-ci-test.yaml b/.github/workflows/cli-ci-test.yaml new file mode 100644 index 0000000..e74ab26 --- /dev/null +++ b/.github/workflows/cli-ci-test.yaml @@ -0,0 +1,55 @@ +name: Integration Test for CLI workflow + +on: + pull_request: + types: + - opened + - synchronize + branches: + - master + paths-ignore: + - "**.md" + - "**.yaml" + - "LICENSE" + - "frontend/**" + - ".github/workflows/frontend-ci-test.yaml" + - ".github/workflows/deploy-gh-pages.yaml" + - ".github/workflows/deploy-gyft-do.yaml" + - "metaploy/**" + - ".gitignore" + - "app.py" + - "wsgi.py" + - ".dockerignore" + - "Dofkerfile" + - "Dofkerfile-dev" + +jobs: + cli-ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Setup env + env: + ENF_FILE : ${{ secrets.ENV_FILE }} + TOKEN_JSON : ${{ secrets.TOKEN_JSON }} + CREDENTIALS_JSON : ${{ secrets.CREDENTIALS_JSON }} + run: | + cat "$ERPCREDS" > erpcreds.py + cat "$TOKEN_JSON" > token.json + cat "$CREDENTIALS_JSON" > credentials.json + + - name: Run the project + run: python gyft.py -D From d7c25ed61c09c08755a0d5e490d58882abe10f89 Mon Sep 17 00:00:00 2001 From: proffapt Date: Tue, 4 Feb 2025 22:55:44 +0530 Subject: [PATCH 7/8] feat: cli ci test --- .github/workflows/cli-ci-test.yaml | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/cli-ci-test.yaml diff --git a/.github/workflows/cli-ci-test.yaml b/.github/workflows/cli-ci-test.yaml new file mode 100644 index 0000000..5964b00 --- /dev/null +++ b/.github/workflows/cli-ci-test.yaml @@ -0,0 +1,55 @@ +name: Integration Test for CLI workflow + +on: + pull_request: + types: + - opened + - synchronize + branches: + - master + paths-ignore: + - "**.md" + - "**.yaml" + - "LICENSE" + - "frontend/**" + - ".github/workflows/frontend-ci-test.yaml" + - ".github/workflows/deploy-gh-pages.yaml" + - ".github/workflows/deploy-gyft-do.yaml" + - "metaploy/**" + - ".gitignore" + - "app.py" + - "wsgi.py" + - ".dockerignore" + - "Dofkerfile" + - "Dofkerfile-dev" + +jobs: + cli-ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Setup env + env: + ERPCREDS : ${{ secrets.ENV_FILE }} + TOKEN_JSON : ${{ secrets.TOKEN_JSON }} + CREDENTIALS_JSON : ${{ secrets.CREDENTIALS_JSON }} + run: | + cat "$ERPCREDS" > erpcreds.py + cat "$TOKEN_JSON" > token.json + cat "$CREDENTIALS_JSON" > credentials.json + + - name: Run the project + run: python gyft.py -D From 7214ec1aa797bb2d7b734f53eeee4c908cb63747 Mon Sep 17 00:00:00 2001 From: proffapt Date: Tue, 4 Feb 2025 22:57:59 +0530 Subject: [PATCH 8/8] feat: cli ci test --- .github/workflows/cli-ci-test.yaml | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/cli-ci-test.yaml diff --git a/.github/workflows/cli-ci-test.yaml b/.github/workflows/cli-ci-test.yaml new file mode 100644 index 0000000..23cf291 --- /dev/null +++ b/.github/workflows/cli-ci-test.yaml @@ -0,0 +1,55 @@ +name: Integration Test for CLI workflow + +on: + pull_request: + types: + - opened + - synchronize + branches: + - master + paths-ignore: + - "**.md" + - "**.yaml" + - "LICENSE" + - "frontend/**" + - ".github/workflows/frontend-ci-test.yaml" + - ".github/workflows/deploy-gh-pages.yaml" + - ".github/workflows/deploy-gyft-do.yaml" + - "metaploy/**" + - ".gitignore" + - "app.py" + - "wsgi.py" + - ".dockerignore" + - "Dofkerfile" + - "Dofkerfile-dev" + +jobs: + cli-ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Setup env + env: + ERPCREDS : ${{ secrets.ERPCREDS }} + TOKEN_JSON : ${{ secrets.TOKEN_JSON }} + CREDENTIALS_JSON : ${{ secrets.CREDENTIALS_JSON }} + run: | + cat "$ERPCREDS" > erpcreds.py + cat "$TOKEN_JSON" > token.json + cat "$CREDENTIALS_JSON" > credentials.json + + - name: Run the project + run: python gyft.py -D