diff --git a/README.md b/README.md index ab3c2c0..146822a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The wrapper requires the user's credentials to authenticate and provide **read-o information accessible on the PESU Academy website. Without the credentials, the wrapper will only be able to fetch details from the `Know Your Class and Section` page. -> :warning: **Warning:** This is not an official API and is not endorsed by PESU. Use at your own risk. +> :warning: **Warning:** This is not an official API and is not endorsed by PES University. Use at your own risk. ## Installation diff --git a/pesuacademy/constants/__init__.py b/pesuacademy/constants/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pesuacademy/constants/fields.py b/pesuacademy/constants/fields.py deleted file mode 100644 index 733684f..0000000 --- a/pesuacademy/constants/fields.py +++ /dev/null @@ -1,30 +0,0 @@ -PROFILE_FIELDS = { - "Personal Details": [ - "Name", - "PESU ID", - "SRN", - "Program", - "Branch", - "Semester", - "Section", - "Email ID", - "Contact No", - "Aadhar No", - "Name as in aadhar" - ], - "Other Information": [ - "SSLC Marks", - "PUC Marks", - "Date Of Birth", - "Blood Group" - ], - "Qualifying examination": [ - "Exam", - "Rank", - "Score" - ], - "Parent Details": [ - "Father Name", - - ] -} diff --git a/pesuacademy/handler.py b/pesuacademy/handler.py new file mode 100644 index 0000000..3cbadd6 --- /dev/null +++ b/pesuacademy/handler.py @@ -0,0 +1,70 @@ +import datetime +from typing import Optional + +import requests_html +from bs4 import BeautifulSoup + +from pesuacademy import pages + + +class PageHandler: + def __init__(self, session: requests_html.HTMLSession): + self.__session = session + self._semester_ids = dict() + self.course_page_handler = pages.CoursesPageHandler() + self.attendance_page_handler = pages.AttendancePageHandler() + self.profile_page_handler = pages.ProfilePageHandler() + + def set_semester_id_to_number_mapping(self, csrf_token: str): + try: + url = "https://www.pesuacademy.com/Academy/a/studentProfilePESU/getStudentSemestersPESU" + query = {"_": str(int(datetime.datetime.now().timestamp() * 1000))} + headers = { + "accept": "*/*", + "accept-language": "en-IN,en-US;q=0.9,en-GB;q=0.8,en;q=0.7", + "content-type": "application/x-www-form-urlencoded", + "referer": "https://www.pesuacademy.com/Academy/s/studentProfilePESU", + "sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "Windows", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "x-csrf-token": csrf_token, + "x-requested-with": "XMLHttpRequest" + } + response = self.__session.get(url, allow_redirects=False, params=query, headers=headers) + if response.status_code != 200: + raise ConnectionError("Unable to fetch course data.") + except Exception: + raise ConnectionError("Unable to fetch course data.") + + option_tags = response.json() + option_tags = BeautifulSoup(option_tags, "lxml") + option_tags = option_tags.find_all("option") + semester_string_ids = list(map(lambda x: x.attrs["value"], option_tags)) + # TODO: Handle CIE semesters (sometimes the tag is + semester_numbers = list(map(lambda x: int(x.text.split("Sem-")[1]), option_tags)) + semesters = dict(zip(semester_numbers, semester_string_ids)) + self._semester_ids = semesters + + def get_semester_ids_from_semester_number(self, semester: Optional[int] = None) -> dict: + """ + Get the semester ids from the semester number. If semester is not provided, all semester ids are returned. + :param semester: The semester number. + :return: The semester ids mapping. + """ + assert semester is None or 1 <= semester <= 8, "Semester number should be between 1 and 8." + return self._semester_ids if semester is None else {semester: self._semester_ids[semester]} + + def get_profile(self): + return self.profile_page_handler.get_page(self.__session) + + def get_courses(self, semester: Optional[int] = None): + semester_ids = self.get_semester_ids_from_semester_number(semester) + return self.course_page_handler.get_page(self.__session, semester_ids) + + def get_attendance(self, semester: Optional[int] = None): + semester_ids = self.get_semester_ids_from_semester_number(semester) + return self.attendance_page_handler.get_page(self.__session, semester_ids) diff --git a/pesuacademy/models/__init__.py b/pesuacademy/models/__init__.py index ff825f5..f8989c6 100644 --- a/pesuacademy/models/__init__.py +++ b/pesuacademy/models/__init__.py @@ -1,2 +1,11 @@ from .course import Course, Attendance -from .profile import Profile +from .profile import ( + Profile, + ClassAndSectionInfo, + PersonalDetails, + OtherInformation, + ParentDetails, + ParentInformation, + AddressDetails, + QualifyingExamination +) diff --git a/pesuacademy/models/profile.py b/pesuacademy/models/profile.py index 9b690fa..c95aaea 100644 --- a/pesuacademy/models/profile.py +++ b/pesuacademy/models/profile.py @@ -1,8 +1,146 @@ -# TODO: Add a list of fields and assign them only. Do not allow any other fields to be added. -# TODO: Make separate profile for KYCAS and Profile +import datetime +from typing import Optional + + +class ClassAndSectionInfo: + def __init__( + self, + prn: str, + srn: str, + name: str, + semester: str, + section: str, + department: str, + branch: str, + institute: str, + cycle: Optional[str] = None + ): + self.prn = prn + self.srn = srn + self.name = name + self.semester = semester + self.section = section + self.cycle = cycle + self.department = department + self.branch = branch + self.institute = institute + + def __str__(self): + return f"{self.__dict__}" + + +class PersonalDetails: + def __init__( + self, + name: str, + prn: str, + srn: str, + branch: str, + semester: str, + section: str, + program: Optional[str] = None, + email: Optional[str] = None, + mobile: Optional[str] = None, + aadhar: Optional[str] = None, + name_as_in_aadhar: Optional[str] = None + ): + self.name = name + self.prn = prn + self.srn = srn + self.program = program + self.branch = branch + self.semester = semester + self.section = section + self.email = email + self.mobile = mobile + self.aadhar = aadhar + self.name_as_in_aadhar = name_as_in_aadhar + + def __str__(self): + return f"{self.__dict__}" + + +class OtherInformation: + def __init__(self, sslc: float, puc: float, dob: datetime.date, blood_group: str): + self.sslc = sslc + self.puc = puc + self.dob = dob + self.blood_group = blood_group + + def __str__(self): + return f"{self.__dict__}" + + +class QualifyingExamination: + def __init__(self, exam: str, rank: int, score: float): + self.exam = exam + self.rank = rank + self.score = score + + def __str__(self): + return f"{self.__dict__}" + + +class ParentInformation: + def __init__( + self, + name: str, + mobile: str, + email: str, + occupation: str, + qualification: str, + designation: str, + employer: str + ): + self.name = name + self.mobile = mobile + self.email = email + self.occupation = occupation + self.qualification = qualification + self.designation = designation + self.employer = employer + + +class ParentDetails: + def __init__( + self, + mother: ParentInformation, + father: ParentInformation + ): + self.mother = mother + self.father = father + + def __str__(self): + return f"{self.__dict__}" + + +def __str__(self): + return f"{self.__dict__}" + + +class AddressDetails: + def __init__(self, present: str, permanent: str): + self.present = present + self.permanent = permanent + + def __str__(self): + return f"{self.__dict__}" + + class Profile: - def __init__(self, **kwargs): - self.__dict__.update(kwargs) + def __init__( + self, + personal_details: PersonalDetails, + other_information: OtherInformation, + qualifying_examination: QualifyingExamination, + parent_details: ParentDetails, + address_details: AddressDetails + ): + self.personal_details = personal_details + self.other_information = other_information + self.qualifying_examination = qualifying_examination + self.parent_details = parent_details + self.address_details = address_details def __str__(self): return f"{self.__dict__}" diff --git a/pesuacademy/pages/__init__.py b/pesuacademy/pages/__init__.py index dec3efb..c667d39 100644 --- a/pesuacademy/pages/__init__.py +++ b/pesuacademy/pages/__init__.py @@ -1 +1,3 @@ -from .profile import get_profile_page +from .attendance import AttendancePageHandler +from .courses import CoursesPageHandler +from .profile import ProfilePageHandler diff --git a/pesuacademy/pages/attendance.py b/pesuacademy/pages/attendance.py index 1a34836..c25fbc9 100644 --- a/pesuacademy/pages/attendance.py +++ b/pesuacademy/pages/attendance.py @@ -7,51 +7,56 @@ from pesuacademy.models import Course, Attendance -def get_attendance_in_semester(session: requests_html.HTMLSession, semester_value: Optional[int] = None): - try: - url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" - query = { - "menuId": "660", - "controllerMode": "6407", - "actionType": "8", - "batchClassId": f"{semester_value}", - "_": str(int(datetime.datetime.now().timestamp() * 1000)), - } - response = session.get(url, allow_redirects=False, params=query) - if response.status_code != 200: - raise ConnectionError("Unable to fetch attendance data.") - soup = BeautifulSoup(response.text, "lxml") - except Exception: - raise ConnectionError("Unable to fetch profile data.") +class AttendancePageHandler: + @staticmethod + def get_attendance_in_semester(session: requests_html.HTMLSession, semester_value: Optional[int] = None): + try: + url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" + query = { + "menuId": "660", + "controllerMode": "6407", + "actionType": "8", + "batchClassId": f"{semester_value}", + "_": str(int(datetime.datetime.now().timestamp() * 1000)), + } + response = session.get(url, allow_redirects=False, params=query) + if response.status_code != 200: + raise ConnectionError("Unable to fetch attendance data.") + soup = BeautifulSoup(response.text, "lxml") + except Exception: + raise ConnectionError("Unable to fetch profile data.") - attendance = [] - table = soup.find("table", attrs={"class": "table box-shadow"}) - table_body = table.find("tbody") - for row in table_body.find_all("tr"): - columns = row.find_all("td") - if len(columns) == 1 and columns[0].text.strip() == 'Data Not\n\t\t\t\t\tAvailable': - break - course_code = columns[0].text.strip() - course_title = columns[1].text.strip() - attended_and_total_classes = columns[2].text.strip() - if '/' in attended_and_total_classes: - attended_classes, total_classes = list(map(int, attended_and_total_classes.split('/'))) - else: - attended_classes, total_classes = None, None - percentage = columns[3].text.strip() - percentage = float(percentage) if percentage != "NA" else None - course = Course(course_code, course_title, attendance=Attendance(attended_classes, total_classes, percentage)) - attendance.append(course) - return attendance + attendance = [] + table = soup.find("table", attrs={"class": "table box-shadow"}) + table_body = table.find("tbody") + for row in table_body.find_all("tr"): + columns = row.find_all("td") + if len(columns) == 1 and columns[0].text.strip() == 'Data Not\n\t\t\t\t\tAvailable': + break + course_code = columns[0].text.strip() + course_title = columns[1].text.strip() + attended_and_total_classes = columns[2].text.strip() + if '/' in attended_and_total_classes: + attended_classes, total_classes = list(map(int, attended_and_total_classes.split('/'))) + else: + attended_classes, total_classes = None, None + percentage = columns[3].text.strip() + percentage = float(percentage) if percentage != "NA" else None + course = Course(course_code, course_title, + attendance=Attendance(attended_classes, total_classes, percentage)) + attendance.append(course) + return attendance - -def get_attendance_page( - session: requests_html.HTMLSession, - semester_ids: dict -) -> dict[int, list[Course]]: - attendance = dict() - for semester_number in semester_ids: - attendance_in_semester = get_attendance_in_semester(session, semester_ids[semester_number]) - attendance[semester_number] = attendance_in_semester - attendance = dict(sorted(attendance.items())) - return attendance + @staticmethod + def get_page( + session: requests_html.HTMLSession, + semester_ids: dict + ) -> dict[int, list[Course]]: + attendance = dict() + for semester_number in semester_ids: + attendance_in_semester = AttendancePageHandler.get_attendance_in_semester( + session, semester_ids[semester_number] + ) + attendance[semester_number] = attendance_in_semester + attendance = dict(sorted(attendance.items())) + return attendance diff --git a/pesuacademy/pages/courses.py b/pesuacademy/pages/courses.py index df85369..0036846 100644 --- a/pesuacademy/pages/courses.py +++ b/pesuacademy/pages/courses.py @@ -7,46 +7,48 @@ from pesuacademy.models import Course -def get_courses_in_semester(session: requests_html.HTMLSession, semester_id: Optional[int] = None): - try: - url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" - query = { - "menuId": "653", - "controllerMode": "6403", - "actionType": "38", - "id": f"{semester_id}", - "_": str(int(datetime.datetime.now().timestamp() * 1000)), - } - response = session.get(url, allow_redirects=False, params=query) - if response.status_code != 200: - raise ConnectionError("Unable to fetch profile data.") - soup = BeautifulSoup(response.text, "lxml") - except Exception: - raise ConnectionError("Unable to fetch courses data.") +class CoursesPageHandler: + @staticmethod + def get_courses_in_semester(session: requests_html.HTMLSession, semester_id: Optional[int] = None): + try: + url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" + query = { + "menuId": "653", + "controllerMode": "6403", + "actionType": "38", + "id": f"{semester_id}", + "_": str(int(datetime.datetime.now().timestamp() * 1000)), + } + response = session.get(url, allow_redirects=False, params=query) + if response.status_code != 200: + raise ConnectionError("Unable to fetch profile data.") + soup = BeautifulSoup(response.text, "lxml") + except Exception: + raise ConnectionError("Unable to fetch courses data.") - courses = [] - table = soup.find("table", attrs={"class": "table table-hover box-shadow"}) - table_body = table.find("tbody") - for row in table_body.find_all("tr"): - columns = row.find_all("td") - if len(columns) == 1 and columns[0].text.strip() == 'No\n\t\t\t\t\t\tsubjects found': - break - course_code = columns[0].text.strip() - course_title = columns[1].text.strip() - course_type = columns[2].text.strip() - course_status = columns[3].text.strip() - course = Course(course_code, course_title, course_type, course_status) - courses.append(course) - return courses + courses = [] + table = soup.find("table", attrs={"class": "table table-hover box-shadow"}) + table_body = table.find("tbody") + for row in table_body.find_all("tr"): + columns = row.find_all("td") + if len(columns) == 1 and columns[0].text.strip() == 'No\n\t\t\t\t\t\tsubjects found': + break + course_code = columns[0].text.strip() + course_title = columns[1].text.strip() + course_type = columns[2].text.strip() + course_status = columns[3].text.strip() + course = Course(course_code, course_title, course_type, course_status) + courses.append(course) + return courses - -def get_courses_page( - session: requests_html.HTMLSession, - semester_ids: dict -) -> dict[int, list[Course]]: - courses = dict() - for semester_number in semester_ids: - courses_in_semester = get_courses_in_semester(session, semester_ids[semester_number]) - courses[semester_number] = courses_in_semester - courses = dict(sorted(courses.items())) - return courses + @staticmethod + def get_page( + session: requests_html.HTMLSession, + semester_ids: dict + ) -> dict[int, list[Course]]: + courses = dict() + for semester_number in semester_ids: + courses_in_semester = CoursesPageHandler.get_courses_in_semester(session, semester_ids[semester_number]) + courses[semester_number] = courses_in_semester + courses = dict(sorted(courses.items())) + return courses diff --git a/pesuacademy/pages/profile.py b/pesuacademy/pages/profile.py index e7da064..8e67877 100644 --- a/pesuacademy/pages/profile.py +++ b/pesuacademy/pages/profile.py @@ -3,45 +3,28 @@ import requests_html from bs4 import BeautifulSoup +from pesuacademy import util from pesuacademy.models import Profile -def get_profile_page(session: requests_html.HTMLSession) -> Profile: - try: - profile_url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" - query = { - "menuId": "670", - "url": "studentProfilePESUAdmin", - "controllerMode": "6414", - "actionType": "5", - "id": "0", - "selectedData": "0", - "_": str(int(datetime.datetime.now().timestamp() * 1000)), - } - response = session.get(profile_url, allow_redirects=False, params=query) - if response.status_code != 200: +class ProfilePageHandler: + @staticmethod + def get_page(session: requests_html.HTMLSession) -> Profile: + try: + profile_url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin" + query = { + "menuId": "670", + "url": "studentProfilePESUAdmin", + "controllerMode": "6414", + "actionType": "5", + "id": "0", + "selectedData": "0", + "_": str(int(datetime.datetime.now().timestamp() * 1000)), + } + response = session.get(profile_url, allow_redirects=False, params=query) + if response.status_code != 200: + raise ConnectionError("Unable to fetch profile data.") + soup = BeautifulSoup(response.text, "lxml") + return util.profile.create_profile_object_from_profile_page(soup) + except Exception: raise ConnectionError("Unable to fetch profile data.") - soup = BeautifulSoup(response.text, "lxml") - except Exception: - raise ConnectionError("Unable to fetch profile data.") - - profile = Profile() - for element in soup.find_all("div", attrs={"class": "form-group"}): - key = element.find("label", - attrs={"class": "col-md-12 col-xs-12 control-label lbl-title-light text-left"}) - if key is None: - continue - else: - key = key.text.strip() - if element.text.strip() in ["Email ID", "Contact No", "Aadhar No", "Name as in aadhar"]: - value_tag = "input" - value_class_name = "form-control" - value = element.find(value_tag, attrs={"class": value_class_name}).attrs["value"].strip() - else: - value_tag = "label" - value_class_name = "col-md-12 col-xs-12 control-label text-left" - value = element.find(value_tag, attrs={"class": value_class_name}).text.strip() - # TODO: Convert DOB to datetime - # TODO: Convert numbers to floats/integers - setattr(profile, key, value) - return profile diff --git a/pesuacademy/pages/utils.py b/pesuacademy/pages/utils.py deleted file mode 100644 index f819b47..0000000 --- a/pesuacademy/pages/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -import datetime -from typing import Optional - -import requests_html -from bs4 import BeautifulSoup - - -def get_semester_list(session: requests_html.HTMLSession, csrf_token: str, semester: Optional[int] = None): - try: - url = "https://www.pesuacademy.com/Academy/a/studentProfilePESU/getStudentSemestersPESU" - query = {"_": str(int(datetime.datetime.now().timestamp() * 1000))} - headers = { - "accept": "*/*", - "accept-language": "en-IN,en-US;q=0.9,en-GB;q=0.8,en;q=0.7", - "content-type": "application/x-www-form-urlencoded", - "referer": "https://www.pesuacademy.com/Academy/s/studentProfilePESU", - "sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "Windows", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-origin", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", - "x-csrf-token": csrf_token, - "x-requested-with": "XMLHttpRequest" - } - response = session.get(url, allow_redirects=False, params=query, headers=headers) - if response.status_code != 200: - raise ConnectionError("Unable to fetch course data.") - except Exception: - raise ConnectionError("Unable to fetch course data.") - - option_tags = response.json() - option_tags = BeautifulSoup(option_tags, "lxml") - option_tags = option_tags.find_all("option") - semester_string_ids = list(map(lambda x: x.attrs["value"], option_tags)) - # TODO: Handle CIE semesters (sometimes the tag is - semester_numbers = list(map(lambda x: int(x.text.split("Sem-")[1]), option_tags)) - semesters = dict(zip(semester_numbers, semester_string_ids)) - return semesters diff --git a/pesuacademy/pesuacademy.py b/pesuacademy/pesuacademy.py index dbd18b0..bfedc9f 100644 --- a/pesuacademy/pesuacademy.py +++ b/pesuacademy/pesuacademy.py @@ -3,9 +3,10 @@ import requests_html from bs4 import BeautifulSoup +from pesuacademy import util +from pesuacademy.handler import PageHandler from .exceptions import CSRFTokenError, AuthenticationError -from .models import Profile, Course -from .pages import profile, courses, attendance, utils +from .models import Profile, ClassAndSectionInfo, Course class PESUAcademy: @@ -21,25 +22,18 @@ def __init__(self, username: Optional[str] = None, password: Optional[str] = Non """ self.__session = requests_html.HTMLSession() self._authenticated: bool = False - self._semester_ids = dict() + self.page_handler = PageHandler(self.__session) self._csrf_token: str = self.generate_csrf_token(username, password) @property def authenticated(self): return self._authenticated - def get_semester_ids_from_semester_number(self, semester: Optional[int] = None) -> dict: - """ - Get the semester ids from the semester number. If semester is not provided, all semester ids are returned. - :param semester: The semester number. - :return: - """ - assert semester is None or 1 <= semester <= 8, "Semester number should be between 1 and 8." - return self._semester_ids if semester is None else {semester: self._semester_ids[semester]} - def generate_csrf_token(self, username: Optional[str] = None, password: Optional[str] = None) -> str: """ Generate a CSRF token. If username and password are provided, authenticate and get the CSRF token. + :param username: Your SRN, PRN or email address. + :param password: Your password. :return: The CSRF token. """ try: @@ -75,11 +69,11 @@ def generate_csrf_token(self, username: Optional[str] = None, password: Optional # if login is successful, update the CSRF token csrf_token = soup.find("meta", attrs={"name": "csrf-token"})["content"] self._authenticated = True - self._semester_ids = utils.get_semester_list(self.__session, csrf_token) + self.page_handler.set_semester_id_to_number_mapping(csrf_token) return csrf_token - def know_your_class_and_section(self, username: str) -> Profile: + def know_your_class_and_section(self, username: str) -> ClassAndSectionInfo: """ Get the publicly visible class and section information of a student from the Know Your Class and Section page. :param username: The SRN, PRN or email address of the student. @@ -112,12 +106,7 @@ def know_your_class_and_section(self, username: str) -> Profile: raise ValueError("Unable to get profile from Know Your Class and Section.") soup = BeautifulSoup(response.text, "html.parser") - profile = Profile() - for th, td in zip(soup.find_all("th"), soup.find_all("td")): - key = th.text.strip() - value = td.text.strip() - setattr(profile, key, value) - + profile = util.profile.create_class_and_section_object_from_know_your_class_and_section(soup) return profile def profile(self) -> Profile: @@ -127,7 +116,7 @@ def profile(self) -> Profile: """ if not self._authenticated: raise AuthenticationError("You need to authenticate first.") - profile_info = profile.get_profile_page(self.__session) + profile_info = self.page_handler.get_profile() return profile_info def courses(self, semester: Optional[int] = None) -> dict[int, list[Course]]: @@ -138,8 +127,7 @@ def courses(self, semester: Optional[int] = None) -> dict[int, list[Course]]: """ if not self._authenticated: raise AuthenticationError("You need to authenticate first.") - semester_ids = self.get_semester_ids_from_semester_number(semester) - courses_info = courses.get_courses_page(self.__session, semester_ids) + courses_info = self.page_handler.get_courses(semester) return courses_info def attendance(self, semester: Optional[int] = None) -> dict[int, list[Course]]: @@ -150,6 +138,5 @@ def attendance(self, semester: Optional[int] = None) -> dict[int, list[Course]]: """ if not self._authenticated: raise AuthenticationError("You need to authenticate first.") - semester_ids = self.get_semester_ids_from_semester_number(semester) - attendance_info = attendance.get_attendance_page(self.__session, semester_ids) + attendance_info = self.page_handler.get_attendance(semester) return attendance_info diff --git a/pesuacademy/util/__init__.py b/pesuacademy/util/__init__.py new file mode 100644 index 0000000..44824cb --- /dev/null +++ b/pesuacademy/util/__init__.py @@ -0,0 +1 @@ +from pesuacademy.util import profile diff --git a/pesuacademy/util/profile.py b/pesuacademy/util/profile.py new file mode 100644 index 0000000..17ca534 --- /dev/null +++ b/pesuacademy/util/profile.py @@ -0,0 +1,187 @@ +import datetime + +from bs4 import BeautifulSoup + +from pesuacademy.models import ( + ClassAndSectionInfo, + Profile, + PersonalDetails, + OtherInformation, + QualifyingExamination, + ParentDetails, + ParentInformation, + AddressDetails +) + + +def get_data_from_section(soup: BeautifulSoup) -> dict: + """ + Get the data from a row in the Profile page. + :param soup: The BeautifulSoup object of the row. + :return: The data in the row. + """ + data = dict() + for element in soup.find_all("div", attrs={"class": "form-group"}): + key = element.find("label", + attrs={"class": "col-md-12 col-xs-12 control-label lbl-title-light text-left"}) + if key is None: + continue + else: + key = key.text.strip().lower().replace(" ", "_") + + value_tag = "label" + value_class_name = "col-md-12 col-xs-12 control-label text-left" + value = element.find(value_tag, attrs={"class": value_class_name}).text.strip() + value = None if value == "NA" else value + data[key] = value + return data + + +def create_class_and_section_object_from_know_your_class_and_section(soup: BeautifulSoup) -> ClassAndSectionInfo: + """ + Create a ClassAndSectionInfo object from the Know Your Class and Section page. + :param soup: The BeautifulSoup object of the page. + :return: The ClassAndSectionInfo object. + """ + profile_data = dict() + for th, td in zip(soup.find_all("th"), soup.find_all("td")): + key = th.text.strip().lower() + value = td.text.strip() + value = None if value == "NA" else value + if key == "class": + key = "semester" + elif key == "institute name": + key = "institute" + profile_data[key] = value + return ClassAndSectionInfo(**profile_data) + + +def create_personal_details_object_from_profile_page(soup: BeautifulSoup) -> PersonalDetails: + """ + Create a PersonalDetails object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The PersonalDetails object. + """ + personal_details = dict() + personal_details_section = soup.find("div", attrs={"class": "elem-info-wrapper box-shadow clearfix"}) + for element in personal_details_section.find_all("div", attrs={"class": "form-group"}): + key = element.find("label", + attrs={"class": "col-md-12 col-xs-12 control-label lbl-title-light text-left"}) + if key is None: + continue + else: + key = key.text.strip().lower().replace(" ", "_") + + if element.text.strip() in ["Email ID", "Contact No", "Aadhar No", "Name as in aadhar"]: + value_tag = "input" + value_class_name = "form-control" + value = element.find(value_tag, attrs={"class": value_class_name}).attrs["value"].strip() + else: + value_tag = "label" + value_class_name = "col-md-12 col-xs-12 control-label text-left" + value = element.find(value_tag, attrs={"class": value_class_name}).text.strip() + + value = None if value == "NA" else value + personal_details[key] = value + + return PersonalDetails( + name=personal_details["name"], + prn=personal_details["pesu_id"], + srn=personal_details["srn"], + program=personal_details["program"], + branch=personal_details["branch"], + semester=personal_details["semester"], + section=personal_details["section"], + email=personal_details["email_id"], + mobile=personal_details["contact_no"], + aadhar=personal_details["aadhar_no"], + name_as_in_aadhar=personal_details["name_as_in_aadhar"] + ) + + +def create_other_information_object_from_profile_page(soup: BeautifulSoup) -> OtherInformation: + """ + Create an OtherInformation object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The OtherInformation object. + """ + other_information_section = soup.find_all("div", attrs={"class": "dashboard-info-bar box-shadow"})[0] + other_information = get_data_from_section(other_information_section) + return OtherInformation( + sslc=float(other_information["sslc_marks"]), + puc=float(other_information["puc_marks"]), + dob=datetime.datetime.strptime(other_information["date_of_birth"], "%d- %m- %Y").date(), + blood_group=other_information["blood_group"] + ) + + +def create_qualifying_examination_object_from_profile_page(soup: BeautifulSoup) -> QualifyingExamination: + """ + Create a QualifyingExamination object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The QualifyingExamination object. + """ + qualifying_examination_section = soup.find_all("div", attrs={"class": "dashboard-info-bar box-shadow"})[1] + qualifying_examination = get_data_from_section(qualifying_examination_section) + return QualifyingExamination( + exam=qualifying_examination["exam"], + rank=int(qualifying_examination["rank"]), + score=float(qualifying_examination["score"]) if qualifying_examination["score"] is not None else None + ) + + +def create_parent_details_object_from_profile_page(soup: BeautifulSoup) -> ParentDetails: + """ + Create a ParentDetails object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The ParentDetails object. + """ + parent_details = {"mother": None, "father": None} + parent_details_section = soup.find_all("div", attrs={"class": "elem-info-wrapper box-shadow clearfix"})[1] + for parent_section in parent_details_section.find_all("div", attrs={"class": "col-md-6"}): + parent_data = get_data_from_section(parent_section) + parent_type = "mother" if "mother_name" in parent_data else "father" + parent_details[parent_type] = ParentInformation( + name=parent_data[f"{parent_type}_name"], + mobile=parent_data["mobile"], + email=parent_data["email"], + occupation=parent_data["occupation"], + qualification=parent_data["qualification"], + designation=parent_data["designation"], + employer=parent_data["employer"] + ) + return ParentDetails( + mother=parent_details["mother"], + father=parent_details["father"] + ) + + +def create_address_details_object_from_profile_page(soup: BeautifulSoup) -> AddressDetails: + """ + Create an AddressDetails object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The AddressDetails object. + """ + address_details_section = soup.find_all("div", attrs={"class": "dashboard-info-bar box-shadow"})[2] + address_details = get_data_from_section(address_details_section) + return AddressDetails(present=address_details["present_address"], permanent=address_details["permanent_address"]) + + +def create_profile_object_from_profile_page(soup: BeautifulSoup) -> Profile: + """ + Create a Profile object from the Profile page. + :param soup: The BeautifulSoup object of the page. + :return: The Profile object. + """ + personal_details = create_personal_details_object_from_profile_page(soup) + other_information = create_other_information_object_from_profile_page(soup) + qualifying_examination = create_qualifying_examination_object_from_profile_page(soup) + parent_details = create_parent_details_object_from_profile_page(soup) + address_details = create_address_details_object_from_profile_page(soup) + return Profile( + personal_details=personal_details, + other_information=other_information, + qualifying_examination=qualifying_examination, + parent_details=parent_details, + address_details=address_details + )