Skip to content

Commit

Permalink
Add attendance to API (#2)
Browse files Browse the repository at this point in the history
* Add attendance

* Minor refactor

* Remove unused properties

* Add TODO
  • Loading branch information
aditeyabaral authored Apr 16, 2024
1 parent 897541d commit 8607303
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 51 deletions.
2 changes: 1 addition & 1 deletion pesu_academy/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .course import Course
from .course import Course, Attendance
from .profile import Profile
22 changes: 21 additions & 1 deletion pesu_academy/models/course.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
from typing import Optional


class Attendance:
def __init__(
self,
attended_classes: Optional[int] = None,
total_classes: Optional[int] = None,
percentage: Optional[float] = None
):
self.attended_classes = attended_classes
self.total_classes = total_classes
self.percentage = percentage

def __str__(self):
return f"{self.__dict__}"


class Course:
def __init__(self, code: str, title: str, _type: str, status: str):
def __init__(self, code: str, title: str, _type: Optional[str] = None, status: Optional[str] = None,
attendance: Optional[Attendance] = None):
self.code = code
self.title = title
self.type = _type
self.status = status
self.attendance = attendance

def __str__(self):
return f"{self.__dict__}"
57 changes: 57 additions & 0 deletions pesu_academy/pages/attendance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import datetime
from typing import Optional

import requests_html
from bs4 import BeautifulSoup

from pesu_academy.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.")

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
49 changes: 10 additions & 39 deletions pesu_academy/pages/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@
from pesu_academy.models import Course


def get_courses_in_semester(session: requests_html.HTMLSession, semester_value: Optional[int] = None):
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_value}",
"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 profile data.")
raise ConnectionError("Unable to fetch courses data.")

courses = []
table = soup.find("table", attrs={"class": "table table-hover box-shadow"})
Expand All @@ -40,42 +40,13 @@ def get_courses_in_semester(session: requests_html.HTMLSession, semester_value:
return courses


def get_courses_page(session: requests_html.HTMLSession, csrf_token: str, semester: Optional[int] = None) -> dict[
int, list[Course]]:
try:
profile_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(profile_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")
def get_courses_page(
session: requests_html.HTMLSession,
semester_ids: dict
) -> dict[int, list[Course]]:
courses = dict()
for semester_option_tag in option_tags:
current_value = semester_option_tag.attrs["value"]
current_semester = int(semester_option_tag.text.split("Sem-")[1])
if semester is None or current_semester == semester:
courses_in_semester = get_courses_in_semester(session, current_value)
courses[current_semester] = courses_in_semester

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
40 changes: 40 additions & 0 deletions pesu_academy/pages/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 <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))
return semesters
40 changes: 30 additions & 10 deletions pesu_academy/pesu_academy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from bs4 import BeautifulSoup

from .exceptions import CSRFTokenError, AuthenticationError
from .models import Profile
from .pages import profile, courses
from .models import Profile, Course
from .pages import profile, courses, attendance, utils


class PESUAcademy:
Expand All @@ -21,15 +21,21 @@ 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._csrf_token: str = self.generate_csrf_token(username, password)

@property
def csrf_token(self):
return self._csrf_token
def authenticated(self):
return self._authenticated

@property
def session(self):
return self.__session
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:
"""
Expand Down Expand Up @@ -69,6 +75,7 @@ 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)

return csrf_token

Expand Down Expand Up @@ -113,7 +120,7 @@ def know_your_class_and_section(self, username: str) -> Profile:

return profile

def profile(self):
def profile(self) -> Profile:
"""
Get the private profile information of the currently authenticated user.
:return: The profile information.
Expand All @@ -123,13 +130,26 @@ def profile(self):
profile_info = profile.get_profile_page(self.__session)
return profile_info

def courses(self, semester: Optional[int] = None):
def courses(self, semester: Optional[int] = None) -> dict[int, list[Course]]:
"""
Get the courses of the currently authenticated user.
:param semester: The semester number. If not provided, all courses across all semesters are returned.
:return: The course information for the given semester.
"""
if not self._authenticated:
raise AuthenticationError("You need to authenticate first.")
courses_info = courses.get_courses_page(self.__session, self._csrf_token, semester)
semester_ids = self.get_semester_ids_from_semester_number(semester)
courses_info = courses.get_courses_page(self.__session, semester_ids)
return courses_info

def attendance(self, semester: Optional[int] = None) -> dict[int, list[Course]]:
"""
Get the attendance in courses of the currently authenticated user.
:param semester: The semester number. If not provided, attendance across all semesters are returned.
:return: The attendance information for the given semester.
"""
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)
return attendance_info

0 comments on commit 8607303

Please sign in to comment.