Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Page Loading & Profile Structure #6

Merged
merged 3 commits into from
Apr 19, 2024
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Empty file removed pesuacademy/constants/__init__.py
Empty file.
30 changes: 0 additions & 30 deletions pesuacademy/constants/fields.py

This file was deleted.

70 changes: 70 additions & 0 deletions pesuacademy/handler.py
Original file line number Diff line number Diff line change
@@ -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 <option value="972">CIE - Level2 (Odd Sem)</option>
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)
11 changes: 10 additions & 1 deletion pesuacademy/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
from .course import Course, Attendance
from .profile import Profile
from .profile import (
Profile,
ClassAndSectionInfo,
PersonalDetails,
OtherInformation,
ParentDetails,
ParentInformation,
AddressDetails,
QualifyingExamination
)
146 changes: 142 additions & 4 deletions pesuacademy/models/profile.py
Original file line number Diff line number Diff line change
@@ -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__}"
4 changes: 3 additions & 1 deletion pesuacademy/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .profile import get_profile_page
from .attendance import AttendancePageHandler
from .courses import CoursesPageHandler
from .profile import ProfilePageHandler
97 changes: 51 additions & 46 deletions pesuacademy/pages/attendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading