librus_apix.grades
This module defines functions and data classes for retrieving and managing grade-related data from the Librus API.
Classes: - Gpa: Represents the semestral grade for a specific semester and subject. - Grade: Represents a single grade entry with detailed information. - GradeDescriptive: Represents a descriptive grade entry with detailed information.
Functions: - get_grades: Fetches and returns the grades, semestral averages, and descriptive grades from Librus.
Usage:
from librus_apix.grades import get_grades
try:
# Fetch grades data
numeric_grades, average_grades, descriptive_grades = get_grades(client, sort_by="all")
# Process the grades data as required
...
except ArgumentError as e:
# Handle invalid argument error
...
except ParseError as e:
# Handle parse error
...
1""" 2This module defines functions and data classes for retrieving and managing grade-related data from the Librus API. 3 4Classes: 5 - Gpa: Represents the semestral grade for a specific semester and subject. 6 - Grade: Represents a single grade entry with detailed information. 7 - GradeDescriptive: Represents a descriptive grade entry with detailed information. 8 9Functions: 10 - get_grades: Fetches and returns the grades, semestral averages, and descriptive grades from Librus. 11 12Usage: 13```python 14from librus_apix.grades import get_grades 15 16try: 17 # Fetch grades data 18 numeric_grades, average_grades, descriptive_grades = get_grades(client, sort_by="all") 19 # Process the grades data as required 20 ... 21except ArgumentError as e: 22 # Handle invalid argument error 23 ... 24except ParseError as e: 25 # Handle parse error 26 ... 27``` 28""" 29 30import re 31from collections import defaultdict 32from dataclasses import dataclass 33from typing import DefaultDict, List, Tuple, Union 34 35from bs4 import BeautifulSoup, Tag 36 37from librus_apix.client import Client 38from librus_apix.exceptions import ArgumentError, ParseError 39from librus_apix.helpers import no_access_check 40 41 42@dataclass 43class Gpa: 44 """ 45 Represents the Semestral Grade for a specific semester and subject. 46 47 Attributes: 48 semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). 49 gpa (float | str): The GPA value, which can be a float or a "-" string meaning it's empty. 50 subject (str): The subject for which the GPA is calculated. 51 """ 52 53 semester: int 54 gpa: float | str 55 subject: str 56 57 58@dataclass 59class Grade: 60 """ 61 Represents a single grade entry with detailed information. 62 63 Attributes: 64 title (str): The title of the grade. 65 grade (str): The grade string value (e.g., '2', '4+', etc.). 66 value (float): Property function. Returns calculated float of grade. (e.g., '4.5 for 4+', '2.75 for 3-') 67 counts (bool): Indicates whether the grade counts towards the GPA. 68 date (str): The date when the grade was given. 69 href (str): A URL suffix associated with the grade. 70 desc (str): A detailed description of the grade. 71 semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). 72 category (str): The category of the grade (e.g., 'Homework', 'Exam'). 73 teacher (str): The name of the teacher who gave the grade. 74 weight (int): The weight of the grade in calculating the final score. 75 """ 76 77 title: str 78 grade: str 79 counts: bool 80 date: str 81 href: str 82 desc: str 83 semester: int 84 category: str 85 teacher: str 86 weight: int 87 88 @property 89 def value(self) -> Union[float, str]: 90 """ 91 Calculates and returns the numeric value of the grade based on its string representation. 92 93 Returns: 94 Union[float, str]: The numeric value of the grade or a string indicating it doesn't count. 95 Raises: 96 ValueError: if grade's format is invalid ex. A+, B+ instead of 5+, 4+ 97 """ 98 if self.counts is False: 99 return "Does not count" 100 try: 101 if len(self.grade) > 1: 102 grade_value = float(self.grade[0]) + float( 103 self.grade[1].replace("+", ".5").replace("-", "-0.25") 104 ) 105 else: 106 grade_value = float(self.grade) 107 return grade_value 108 except ValueError: 109 raise ValueError("Invalid grade format in .value property func") 110 111 112@dataclass 113class GradeDescriptive: 114 title: str 115 grade: str 116 date: str 117 href: str 118 desc: str 119 semester: int 120 teacher: str 121 122 123def get_grades(client: Client, sort_by: str = "all") -> Tuple[ 124 List[DefaultDict[str, List[Grade]]], 125 DefaultDict[str, List[Gpa]], 126 List[DefaultDict[str, List[GradeDescriptive]]], 127]: 128 """ 129 Fetches and returns the grades, semestral averages and descriptive grades from librus. 130 131 Args: 132 client (Client): The client object used to interact with the server. 133 sort_by (str): The criteria to sort grades. Can be 'all', 'week', or 'last_login'. 134 135 Returns: 136 Tuple: A tuple containing lists of numeric and descriptive grades, and GPA information. 137 138 Raises: 139 ArgumentError: If an invalid sort_by value is provided. 140 ParseError: If there is an error in parsing the grades. 141 """ 142 SORT = { 143 "all": "zmiany_logowanie_wszystkie", 144 "week": "zmiany_logowanie_tydzien", 145 "last_login": "zmiany_logowanie", 146 } 147 if sort_by not in SORT.keys(): 148 raise ArgumentError( 149 "Wrong value for sort_by it can be either all, week or last_login" 150 ) 151 152 tr = no_access_check( 153 BeautifulSoup( 154 client.post(client.GRADES_URL, data={SORT[sort_by]: "1"}).text, 155 "lxml", 156 ) 157 ).find_all("tr", attrs={"class": ["line0", "line1"], "id": None}) 158 if len(tr) < 1: 159 raise ParseError("Error in parsing grades") 160 161 sem_grades, avg_grades = _extract_grades_numeric(tr) 162 sem_grades_desc = _extract_grades_descriptive(tr) 163 return sem_grades, avg_grades, sem_grades_desc 164 165 166def _handle_subject(semester_grades) -> str: 167 return semester_grades[0].text.replace("\n", "").strip() 168 169 170def _get_desc_and_counts(a: Tag, grade: str, subject: str) -> Tuple[str, bool]: 171 desc = f"Ocena: {grade}\nPrzedmiot: {subject}\n" 172 desc += re.sub( 173 r"<br*>", 174 "\n", 175 a.attrs.get("title", "").replace("<br/>", "").replace("<br />", "\n"), 176 ) 177 gpacount = re.search("Licz do średniej: [a-zA-Z]{3}", desc) 178 counts = False 179 if gpacount is not None: 180 pair = gpacount[0].split(": ") 181 if len(pair) >= 2 and pair[1] == "tak": 182 counts = True 183 return desc, counts 184 185 186def _extract_grade_info( 187 a: Tag, subject: str 188) -> Tuple[str, str, str, str, bool, str, str, int]: 189 date = re.search("Data:.{11}", a.attrs.get("title", "")) 190 if date is None: 191 raise ParseError("Error in getting grade's date.") 192 193 attr_dict = {} 194 for attr in a.attrs["title"].replace("<br/>", "<br>").split("<br>"): 195 if len(attr.strip()) >= 2: 196 key, value = attr.split(": ", 1) 197 attr_dict[key] = value 198 category: str = attr_dict.get("Kategoria", "") 199 teacher: str = attr_dict.get("Nauczyciel", "") 200 weight: int = int(attr_dict.get("Waga", 0)) 201 202 grade = a.text.replace("\xa0", "").replace("\n", "") 203 desc, counts = _get_desc_and_counts(a, grade, subject) 204 date = date.group().split(" ") 205 date = date[1] if len(date) >= 2 else " ".join(date) 206 return ( 207 grade, 208 date, 209 a.attrs.get("href", ""), 210 desc, 211 counts, 212 category, 213 teacher, 214 weight, 215 ) 216 217 218def _extract_grades_numeric( 219 table_rows: List[Tag], 220) -> Tuple[List[DefaultDict[str, List[Grade]]], DefaultDict[str, List[Gpa]]]: 221 # list containing two dicts (for each semester) 222 # key of each semester dict is subject, in each subject there is list of grades 223 sem_grades: List[DefaultDict[str, List[Grade]]] = [ 224 defaultdict(list) for _ in range(2) 225 ] # 2 semesters 226 avg_grades: DefaultDict[str, List[Gpa]] = defaultdict(list) 227 228 for box in table_rows: 229 if box.select_one("td[class='center micro screen-only']") is None: 230 # row without grade data - skip 231 continue 232 semester_grades = box.select('td[class!="center micro screen-only"]') 233 if len(semester_grades) < 9: 234 continue 235 average_grades = list(map(lambda x: x.text, box.select("td.right"))) 236 semesters = [semester_grades[1:4], semester_grades[4:7]] 237 subject = _handle_subject(semester_grades) 238 for semester_number, semester in enumerate(semesters): 239 if subject not in sem_grades[semester_number]: 240 sem_grades[semester_number][subject] = [] 241 for sg in semester: 242 grade_a_improved = sg.select( 243 "td[class!='center'] > span > span.grade-box > a" 244 ) 245 grade_a = ( 246 sg.select("td[class!='center'] > span.grade-box > a") 247 + grade_a_improved 248 ) 249 for a in grade_a: 250 ( 251 _grade, 252 date, 253 _href, 254 desc, 255 counts, 256 category, 257 teacher, 258 weight, 259 ) = _extract_grade_info(a, subject) 260 g = Grade( 261 subject, 262 _grade, 263 counts, 264 date, 265 a.attrs.get("href", ""), 266 desc, 267 semester_number + 1, 268 category, 269 teacher, 270 weight, 271 ) 272 sem_grades[semester_number][subject].append(g) 273 avg_gr = ( 274 average_grades[semester_number] 275 if len(average_grades) >= semester_number 276 else 0.0 277 ) # might happen that the list is empty 278 gpa = Gpa(semester_number + 1, avg_gr, subject) 279 avg_grades[subject].append(gpa) 280 avg_gr = ( 281 average_grades[-1] if len(average_grades) > 0 else 0.0 282 ) # might happen that the list is empty 283 avg_grades[subject].append(Gpa(0, avg_gr, subject)) 284 285 return sem_grades, avg_grades 286 287 288def _extract_grades_descriptive( 289 table_rows: List[Tag], 290) -> List[DefaultDict[str, List[GradeDescriptive]]]: 291 # list containing two dicts (for each semester) 292 # key of each semester dict is subject, in each subject there is list of grades 293 sem_grades_desc: List[DefaultDict[str, List[GradeDescriptive]]] = [ 294 defaultdict(list) for _ in range(2) 295 ] # 2 semesters 296 297 for box in table_rows: 298 if box.select_one("td[class='micro center screen-only']") is None: 299 # row without descriptive grade data - skip 300 continue 301 semester_grades = box.select('td[class!="micro center screen-only"]') 302 if len(semester_grades) < 3: 303 continue 304 semesters = [semester_grades[1], semester_grades[2]] 305 subject = semester_grades[0].text.replace("\n", "").strip() 306 for sem_index, sg in enumerate(semesters): 307 grade_a = sg.select("td[class!='center'] > span.grade-box > a") 308 for a in grade_a: 309 ( 310 _grade, 311 date, 312 href, 313 desc, 314 _, 315 _category, 316 teacher, 317 _weight, 318 ) = _extract_grade_info(a, subject) 319 if "javascript" in href: 320 # javascript content is not standard href - clear it 321 href = "" 322 g = GradeDescriptive( 323 subject, _grade, date, href, desc, sem_index + 1, teacher 324 ) 325 sem_grades_desc[sem_index][subject].append(g) 326 327 # get semester descriptive grade 328 found_grade = False 329 summary_title = "" 330 summary_desc = "" 331 summary_date = "" 332 summary_teacher = "" 333 334 parse_next_row = False 335 for box in table_rows: 336 if parse_next_row: 337 parse_next_row = False 338 paragraphs = box.find_all("p") 339 text_list = [par.text.strip() for par in paragraphs] 340 summary_desc = "\n".join(text_list).strip() 341 found_grade = True 342 # description found - break 343 # There is no more grades for now (for first semester). Maybe there will be grade for 344 # second semester, but the format (structure) of web page is unknown for the moment. 345 # #TODO: implement the case for second semester (in future) 346 break 347 348 header = box.select_one("th") 349 if header and header.select_one("strong") is not None: 350 # header row found - next row will contain the description 351 parse_next_row = True 352 title_tag = header.select_one("strong") 353 if title_tag is None: 354 continue 355 info = title_tag.next_sibling 356 if info is None: 357 continue 358 summary_title = title_tag.text.strip() 359 summary_date = re.findall(r"opublikowano: (.+?) ", info.text)[ 360 0 361 ] # get date only 362 summary_teacher = re.findall(r"nauczyciel: (.+?)\)", info.text)[0] 363 364 if found_grade: 365 sem_index = 0 366 semester_summary = GradeDescriptive( 367 summary_title, 368 "", 369 summary_date, 370 "", 371 summary_desc, 372 sem_index + 1, 373 summary_teacher, 374 ) 375 if summary_title not in sem_grades_desc[sem_index]: 376 sem_grades_desc[sem_index][summary_title] = [] 377 sem_grades_desc[sem_index][summary_title].append(semester_summary) 378 379 return sem_grades_desc
43@dataclass 44class Gpa: 45 """ 46 Represents the Semestral Grade for a specific semester and subject. 47 48 Attributes: 49 semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). 50 gpa (float | str): The GPA value, which can be a float or a "-" string meaning it's empty. 51 subject (str): The subject for which the GPA is calculated. 52 """ 53 54 semester: int 55 gpa: float | str 56 subject: str
Represents the Semestral Grade for a specific semester and subject.
Attributes: semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). gpa (float | str): The GPA value, which can be a float or a "-" string meaning it's empty. subject (str): The subject for which the GPA is calculated.
59@dataclass 60class Grade: 61 """ 62 Represents a single grade entry with detailed information. 63 64 Attributes: 65 title (str): The title of the grade. 66 grade (str): The grade string value (e.g., '2', '4+', etc.). 67 value (float): Property function. Returns calculated float of grade. (e.g., '4.5 for 4+', '2.75 for 3-') 68 counts (bool): Indicates whether the grade counts towards the GPA. 69 date (str): The date when the grade was given. 70 href (str): A URL suffix associated with the grade. 71 desc (str): A detailed description of the grade. 72 semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). 73 category (str): The category of the grade (e.g., 'Homework', 'Exam'). 74 teacher (str): The name of the teacher who gave the grade. 75 weight (int): The weight of the grade in calculating the final score. 76 """ 77 78 title: str 79 grade: str 80 counts: bool 81 date: str 82 href: str 83 desc: str 84 semester: int 85 category: str 86 teacher: str 87 weight: int 88 89 @property 90 def value(self) -> Union[float, str]: 91 """ 92 Calculates and returns the numeric value of the grade based on its string representation. 93 94 Returns: 95 Union[float, str]: The numeric value of the grade or a string indicating it doesn't count. 96 Raises: 97 ValueError: if grade's format is invalid ex. A+, B+ instead of 5+, 4+ 98 """ 99 if self.counts is False: 100 return "Does not count" 101 try: 102 if len(self.grade) > 1: 103 grade_value = float(self.grade[0]) + float( 104 self.grade[1].replace("+", ".5").replace("-", "-0.25") 105 ) 106 else: 107 grade_value = float(self.grade) 108 return grade_value 109 except ValueError: 110 raise ValueError("Invalid grade format in .value property func")
Represents a single grade entry with detailed information.
Attributes: title (str): The title of the grade. grade (str): The grade string value (e.g., '2', '4+', etc.). value (float): Property function. Returns calculated float of grade. (e.g., '4.5 for 4+', '2.75 for 3-') counts (bool): Indicates whether the grade counts towards the GPA. date (str): The date when the grade was given. href (str): A URL suffix associated with the grade. desc (str): A detailed description of the grade. semester (int): The semester number (e.g., 1 for first semester, 2 for second semester). category (str): The category of the grade (e.g., 'Homework', 'Exam'). teacher (str): The name of the teacher who gave the grade. weight (int): The weight of the grade in calculating the final score.
89 @property 90 def value(self) -> Union[float, str]: 91 """ 92 Calculates and returns the numeric value of the grade based on its string representation. 93 94 Returns: 95 Union[float, str]: The numeric value of the grade or a string indicating it doesn't count. 96 Raises: 97 ValueError: if grade's format is invalid ex. A+, B+ instead of 5+, 4+ 98 """ 99 if self.counts is False: 100 return "Does not count" 101 try: 102 if len(self.grade) > 1: 103 grade_value = float(self.grade[0]) + float( 104 self.grade[1].replace("+", ".5").replace("-", "-0.25") 105 ) 106 else: 107 grade_value = float(self.grade) 108 return grade_value 109 except ValueError: 110 raise ValueError("Invalid grade format in .value property func")
Calculates and returns the numeric value of the grade based on its string representation.
Returns: Union[float, str]: The numeric value of the grade or a string indicating it doesn't count. Raises: ValueError: if grade's format is invalid ex. A+, B+ instead of 5+, 4+
113@dataclass 114class GradeDescriptive: 115 title: str 116 grade: str 117 date: str 118 href: str 119 desc: str 120 semester: int 121 teacher: str
124def get_grades(client: Client, sort_by: str = "all") -> Tuple[ 125 List[DefaultDict[str, List[Grade]]], 126 DefaultDict[str, List[Gpa]], 127 List[DefaultDict[str, List[GradeDescriptive]]], 128]: 129 """ 130 Fetches and returns the grades, semestral averages and descriptive grades from librus. 131 132 Args: 133 client (Client): The client object used to interact with the server. 134 sort_by (str): The criteria to sort grades. Can be 'all', 'week', or 'last_login'. 135 136 Returns: 137 Tuple: A tuple containing lists of numeric and descriptive grades, and GPA information. 138 139 Raises: 140 ArgumentError: If an invalid sort_by value is provided. 141 ParseError: If there is an error in parsing the grades. 142 """ 143 SORT = { 144 "all": "zmiany_logowanie_wszystkie", 145 "week": "zmiany_logowanie_tydzien", 146 "last_login": "zmiany_logowanie", 147 } 148 if sort_by not in SORT.keys(): 149 raise ArgumentError( 150 "Wrong value for sort_by it can be either all, week or last_login" 151 ) 152 153 tr = no_access_check( 154 BeautifulSoup( 155 client.post(client.GRADES_URL, data={SORT[sort_by]: "1"}).text, 156 "lxml", 157 ) 158 ).find_all("tr", attrs={"class": ["line0", "line1"], "id": None}) 159 if len(tr) < 1: 160 raise ParseError("Error in parsing grades") 161 162 sem_grades, avg_grades = _extract_grades_numeric(tr) 163 sem_grades_desc = _extract_grades_descriptive(tr) 164 return sem_grades, avg_grades, sem_grades_desc
Fetches and returns the grades, semestral averages and descriptive grades from librus.
Args: client (Client): The client object used to interact with the server. sort_by (str): The criteria to sort grades. Can be 'all', 'week', or 'last_login'.
Returns: Tuple: A tuple containing lists of numeric and descriptive grades, and GPA information.
Raises: ArgumentError: If an invalid sort_by value is provided. ParseError: If there is an error in parsing the grades.