librus_apix.attendance
This module provides functions for retrieving attendance records from the Librus site, parsing them, and calculating attendance frequency.
Classes: - Attendance: Represents an attendance record with various attributes such as type, date, teacher, etc.
Functions: - get_detail: Retrieves attendance details from a specific URL suffix. - get_gateway_attendance: Retrieves attendance data from the Librus gateway API. - get_attendance_frequency: Calculates attendance frequency for each semester and overall. - get_attendance: Retrieves attendance records from Librus based on specified sorting criteria.
Usage:
from librus_apix.client import new_client
# Create a new client instance
client = new_client()
client.get_token(username, password)
# Retrieve attendance details
detail_url = "example_detail_url"
attendance_details = get_detail(client, detail_url)
# Retrieve attendance data from the gateway API
gateway_attendance = get_gateway_attendance(client)
# Calculate attendance frequency
first_sem_freq, second_sem_freq, overall_freq = get_attendance_frequency(client)
# Retrieve attendance records sorted by a specific criteria
attendance_records = get_attendance(client, sort_by="all")
1""" 2This module provides functions for retrieving attendance records from the Librus site, parsing them, and calculating attendance frequency. 3 4Classes: 5 - Attendance: Represents an attendance record with various attributes such as type, date, teacher, etc. 6 7Functions: 8 - get_detail: Retrieves attendance details from a specific URL suffix. 9 - get_gateway_attendance: Retrieves attendance data from the Librus gateway API. 10 - get_attendance_frequency: Calculates attendance frequency for each semester and overall. 11 - get_attendance: Retrieves attendance records from Librus based on specified sorting criteria. 12 13Usage: 14```python 15from librus_apix.client import new_client 16 17# Create a new client instance 18client = new_client() 19client.get_token(username, password) 20 21# Retrieve attendance details 22detail_url = "example_detail_url" 23attendance_details = get_detail(client, detail_url) 24 25# Retrieve attendance data from the gateway API 26gateway_attendance = get_gateway_attendance(client) 27 28# Calculate attendance frequency 29first_sem_freq, second_sem_freq, overall_freq = get_attendance_frequency(client) 30 31# Retrieve attendance records sorted by a specific criteria 32attendance_records = get_attendance(client, sort_by="all") 33``` 34""" 35 36import asyncio 37from collections import defaultdict 38from collections.abc import Coroutine 39from dataclasses import dataclass 40from datetime import datetime 41from typing import Any, Dict, List, Optional, Tuple 42 43from aiohttp import ClientSession 44from bs4 import BeautifulSoup, NavigableString, Tag 45 46from librus_apix.client import Client 47from librus_apix.exceptions import ArgumentError, ParseError 48from librus_apix.helpers import no_access_check 49 50 51@dataclass 52class Attendance: 53 """ 54 Represents an attendance record. 55 56 Attributes: 57 symbol (str): The symbol representing the attendance record. 58 href (str): The URL associated with the attendance record. 59 semester (int): The semester number to which the attendance record belongs. 60 date (str): The date of the attendance record. 61 type (str): The type of attendance (e.g., absence, presence). 62 teacher (str): The name of the teacher associated with the attendance record. 63 period (int): The period or hour of the attendance record. 64 excursion (bool): Indicates if the attendance record is related to an excursion. 65 topic (str): The topic or subject of the attendance record. 66 subject (str): The school subject associated with the attendance record. 67 """ 68 69 symbol: str 70 href: str 71 semester: int 72 date: str 73 type: str 74 teacher: str 75 period: int 76 excursion: bool 77 topic: str 78 subject: str 79 80 81def get_detail(client: Client, detail_url: str) -> Dict[str, str]: 82 """ 83 Retrieves attendance details from the specified detail URL suffix. 84 85 Args: 86 client (Client): The client object used to make the request. 87 detail_url (str): The URL for fetching the attendance details. 88 89 Returns: 90 Dict[str, str]: A dictionary containing the attendance details. 91 92 Raises: 93 ParseError: If there is an error parsing the attendance details. 94 """ 95 details = {} 96 div = no_access_check( 97 BeautifulSoup( 98 client.get(client.ATTENDANCE_DETAILS_URL + detail_url).text, "lxml" 99 ) 100 ).find("div", attrs={"class": "container-background"}) 101 if div is None or isinstance(div, NavigableString): 102 raise ParseError("Error in parsing attendance details") 103 line = div.find_all("tr", attrs={"class": ["line0", "line1"]}) 104 if len(line) < 1: 105 raise ParseError("Error in parsing attendance details (Lines are empty).") 106 for l in line: 107 th = l.find("th") 108 td = l.find("td") 109 if th is None or td is None: 110 continue 111 details[l.find("th").text] = l.find("td").text 112 return details 113 114 115async def _get_subject_attendance( 116 client, start: Optional[datetime] = None, end: Optional[datetime] = None 117): 118 types = { 119 "1": "nb", 120 "2": "sp", 121 "3": "u", 122 "4": "zw", 123 "100": "ob", 124 "1266": "wy", 125 "2022": "k", 126 "2829": "sz", 127 } 128 129 client.refresh_oauth() 130 attendances = client.get(client.GATEWAY_API_ATTENDANCE).json()["Attendances"] 131 132 if start is not None or end is not None: 133 filtered = [] 134 for a in attendances: 135 date = datetime.strptime(a["Date"], "%Y-%m-%d") 136 if start is not None and date < start: 137 continue 138 if end is not None and date > end: 139 continue 140 filtered.append(a) 141 attendances = filtered 142 143 cookies = client._session.cookies 144 headers = client._session.headers 145 base_url = client.BASE_URL 146 147 lesson_cache = {} 148 subject_cache = {} 149 150 async def req(session: ClientSession, url: str, retries=5, delay=0.5): 151 for attempt in range(retries): 152 try: 153 async with session.get(url) as response: 154 response.raise_for_status() 155 return await response.json() 156 except: 157 if attempt == retries - 1: 158 raise 159 await asyncio.sleep(delay * (2**attempt)) 160 161 async def _lesson_attendance(session: ClientSession, attendance: dict): 162 lesson_id = attendance["Lesson"]["Id"] 163 absence_type = types.get(str(attendance["Type"]["Id"]), "unknown") 164 165 if lesson_id in lesson_cache: 166 return (lesson_cache[lesson_id], absence_type) 167 168 lesson_resp = await req( 169 session, f"{base_url}/gateway/api/2.0/Lessons/{lesson_id}" 170 ) 171 sub_id = lesson_resp["Lesson"]["Subject"]["Id"] 172 173 if sub_id in subject_cache: 174 subject_name = subject_cache[sub_id] 175 else: 176 sub_resp = await req( 177 session, f"{base_url}/gateway/api/2.0/Subjects/{sub_id}" 178 ) 179 subject_name = sub_resp["Subject"]["Name"] 180 subject_cache[sub_id] = subject_name 181 182 lesson_cache[lesson_id] = subject_name 183 return (subject_name, absence_type) 184 185 async with ClientSession(cookies=cookies, headers=headers) as session: 186 results = await asyncio.gather( 187 *[_lesson_attendance(session, attendance) for attendance in attendances] 188 ) 189 190 counts = defaultdict(lambda: defaultdict(int)) 191 for subject, absence in results: 192 counts[subject][absence] += 1 193 194 return {subject: dict(types) for subject, types in counts.items()} 195 196 197def get_subject_frequency( 198 client: Client, 199 attendances=None, 200 start: Optional[datetime] = None, 201 end: Optional[datetime] = None, 202) -> Dict[str, float]: 203 if not attendances: 204 attendances = asyncio.run(_get_subject_attendance(client, start, end)) 205 frequency = {} 206 for sub in attendances: 207 attended = attendances[sub].get("ob", 0) + attendances[sub].get("sp", 0) 208 unattended = ( 209 attendances[sub].get("nb", 0) 210 + attendances[sub].get("u", 0) 211 + attendances[sub].get("zw", 0) 212 ) 213 total = attended + unattended 214 frequency[sub] = round(attended / total * 100, 2) if total > 0 else 100.0 215 return frequency 216 217 218def get_gateway_attendance(client: Client) -> List[Tuple[Tuple[str, str], str, str]]: 219 """ 220 Retrieves attendance data from the gateway API. 221 222 The gateway API data is typically updated every 3 hours. 223 Accessing api.librus.pl requires a private key. 224 225 Requires: 226 oauth token to be refreshed with client.refresh_oauth() 227 228 Args: 229 client (Client): The client object used to make the request. 230 231 Returns: 232 List[Tuple[Tuple[str, str], str, str]]: A list of tuples containing attendance data. 233 Each tuple contains three elements: 234 1. Tuple containing type abbreviation and type name. 235 2. Lesson number. 236 3. Semester. 237 238 Raises: 239 ValueError: If the OAuth token is missing. 240 AuthorizationError: If there is an authorization error while accessing the API. 241 """ 242 types = { 243 "1": {"short": "nb", "name": "Nieobecność"}, 244 "2": {"short": "sp", "name": "Spóźnienie"}, 245 "3": {"short": "u", "name": "Nieobecność uspr."}, 246 "4": {"short": "zw", "name": "Zwolnienie"}, 247 "100": {"short": "ob", "name": "Obecność"}, 248 "1266": {"short": "wy", "name": "Wycieczka"}, 249 "2022": {"short": "k", "name": "Konkurs szkolny"}, 250 "2829": {"short": "sz", "name": "Szkolenie"}, 251 } 252 oauth = client.token.oauth 253 if oauth == "": 254 oauth = client.refresh_oauth() 255 client.cookies["oauth_token"] = oauth 256 response = client.get(client.GATEWAY_API_ATTENDANCE) 257 258 attendances = response.json()["Attendances"] 259 _attendance = [] 260 for a in attendances: 261 type_id = a["Type"]["Id"] 262 type_data = tuple(types[str(type_id)].values()) 263 lesson_number = a["LessonNo"] 264 semester = a["Semester"] 265 266 _attendance.append((type_data, lesson_number, semester)) 267 268 return _attendance 269 270 271def get_attendance_frequency(client: Client) -> Tuple[float, float, float]: 272 """ 273 Calculates the attendance frequency for each semester and overall. 274 275 Args: 276 client (Client): The client object used to retrieve attendance data. 277 278 Returns: 279 Tuple[float, float, float]: A tuple containing the attendance frequencies for the first semester, second semester, and overall. 280 Each frequency is a float value between 0 and 1, representing the ratio of attended lessons to total lessons. 281 282 Raises: 283 ValueError: If there is an error retrieving attendance data. 284 """ 285 attendance = get_gateway_attendance(client) 286 first_semester = [a for a in attendance if a[2] == 1] 287 second_semester = [a for a in attendance if a[2] == 2] 288 f_attended = len([a for a in first_semester if a[0][0] in ["wy", "ob", "sp"]]) 289 s_attended = len([a for a in second_semester if a[0][0] in ["wy", "ob", "sp"]]) 290 f_freq = f_attended / len(first_semester) if len(first_semester) != 0 else 1 291 s_freq = s_attended / len(second_semester) if len(second_semester) != 0 else 1 292 overall_freq = ( 293 len([a for a in attendance if a[0][0] in ["wy", "ob", "sp"]]) / len(attendance) 294 if len(attendance) != 0 295 else 1 296 ) 297 return f_freq, s_freq, overall_freq 298 # ADD Lesson frequency 299 300 301def _extract_title_pairs(title: str): 302 sanitize_title = ( 303 title.replace("</b>", "<br>").replace("<br/>", "").strip().split("<br>") 304 ) 305 306 return [pair.split(":", 1) for pair in sanitize_title] 307 308 309def _sanitize_pairs(pairs: List[List[str]]) -> Dict[str, str]: 310 sanitized_pairs = {} 311 for pair in pairs: 312 if len(pair) != 2: 313 sanitized_pairs[pair[0].strip()] = "unknown" 314 continue 315 key, val = pair 316 sanitized_pairs[key.strip()] = val.strip() 317 return sanitized_pairs 318 319 320def _sanitize_onclick_href(onclick: str): 321 href = ( 322 onclick.replace("otworz_w_nowym_oknie(", "") 323 .split(",")[0] 324 .replace("'", "") 325 .split("/") 326 ) 327 if len(href) < 4: 328 return "" 329 return href[3] 330 331 332def _create_attendance(single: Tag, semester: int): 333 """ 334 Creates an Attendance object from a single attendance record. 335 336 Args: 337 single (Tag): The BeautifulSoup Tag representing a single attendance record. 338 semester (int): The semester number to which the attendance record belongs. 339 340 Returns: 341 Attendance: An Attendance object representing the parsed attendance record. 342 343 Raises: 344 ParseError: If there is an error parsing the attendance record. 345 """ 346 if single.attrs.get("title") is None: 347 raise ParseError("Absence anchor title is None") 348 pairs = _extract_title_pairs(single.attrs["title"]) 349 attributes = _sanitize_pairs(pairs) 350 351 date = attributes.get("Data", "").split(" ")[0] 352 _type = attributes.get("Rodzaj", "") 353 school_subject = attributes.get("Lekcja", "") 354 topic = attributes.get("Temat zajęć", "") 355 period = int(attributes.get("Godzina lekcyjna", "0")) 356 excursion = True if attributes.get("Czy wycieczka", "") == "Tak" else False 357 teacher = attributes.get("Nauczyciel", "") 358 359 href = _sanitize_onclick_href(single.attrs.get("onclick", "")) 360 361 return Attendance( 362 single.text, 363 href, 364 semester, 365 date, 366 _type, 367 teacher, 368 period, 369 excursion, 370 topic, 371 school_subject, 372 ) 373 374 375def get_attendance(client: Client, sort_by: str = "all") -> List[List[Attendance]]: 376 """ 377 Retrieves attendance records from librus. 378 379 Args: 380 client (Client): The client object used to fetch attendance data. 381 sort_by (str, optional): The sorting criteria for attendance records. 382 It can be one of the following values: 383 - "all": Sort by all attendance records. 384 - "week": Sort by attendance records for the current week. 385 - "last_login": Sort by attendance records since the last login. 386 Defaults to "all". 387 388 Returns: 389 List[List[Attendance]]: A list containing attendance records grouped by semester. 390 Each inner list represents attendance records for a specific semester. 391 392 Raises: 393 ArgumentError: If an invalid value is provided for the sort_by parameter. 394 ParseError: If there is an error parsing the attendance data. 395 """ 396 SORT: Dict[str, Dict[str, str]] = { 397 "all": {"zmiany_logowanie_wszystkie": ""}, 398 "week": {"zmiany_logowanie_tydzien": "zmiany_logowanie_tydzien"}, 399 "last_login": {"zmiany_logowanie": "zmiany_logowanie"}, 400 } 401 if sort_by not in SORT.keys(): 402 raise ArgumentError( 403 "Wrong value for sort_by it can be either all, week or last_login" 404 ) 405 406 soup = no_access_check( 407 BeautifulSoup( 408 client.post(client.ATTENDANCE_URL, data=SORT[sort_by]).text, 409 "lxml", 410 ) 411 ) 412 table = soup.find("table", attrs={"class": "center big decorated"}) 413 if table is None or isinstance(table, NavigableString): 414 raise ParseError("Error parsing attendance (table).") 415 416 days = table.find_all("tr", attrs={"class": ["line0", "line1"]}) 417 attendance_semesters = [[] for _ in range(2)] # Two semesters 418 semester = -1 419 for day in days: 420 if day.find("td", attrs={"class": "center bolded"}): 421 # marker to increment semester 422 semester += 1 423 attendance = day.find_all("td", attrs={"class": "center"}) 424 for absence in attendance: 425 a_elem: List[Tag] = absence.find_all("a") 426 for single in a_elem: 427 attendance_semesters[semester].append( 428 _create_attendance(single, semester) 429 ) 430 match semester: 431 case 0: 432 return list(attendance_semesters) 433 case 1: 434 return list(reversed(attendance_semesters)) 435 case _: 436 raise ParseError("Couldn't find attendance semester")
52@dataclass 53class Attendance: 54 """ 55 Represents an attendance record. 56 57 Attributes: 58 symbol (str): The symbol representing the attendance record. 59 href (str): The URL associated with the attendance record. 60 semester (int): The semester number to which the attendance record belongs. 61 date (str): The date of the attendance record. 62 type (str): The type of attendance (e.g., absence, presence). 63 teacher (str): The name of the teacher associated with the attendance record. 64 period (int): The period or hour of the attendance record. 65 excursion (bool): Indicates if the attendance record is related to an excursion. 66 topic (str): The topic or subject of the attendance record. 67 subject (str): The school subject associated with the attendance record. 68 """ 69 70 symbol: str 71 href: str 72 semester: int 73 date: str 74 type: str 75 teacher: str 76 period: int 77 excursion: bool 78 topic: str 79 subject: str
Represents an attendance record.
Attributes: symbol (str): The symbol representing the attendance record. href (str): The URL associated with the attendance record. semester (int): The semester number to which the attendance record belongs. date (str): The date of the attendance record. type (str): The type of attendance (e.g., absence, presence). teacher (str): The name of the teacher associated with the attendance record. period (int): The period or hour of the attendance record. excursion (bool): Indicates if the attendance record is related to an excursion. topic (str): The topic or subject of the attendance record. subject (str): The school subject associated with the attendance record.
82def get_detail(client: Client, detail_url: str) -> Dict[str, str]: 83 """ 84 Retrieves attendance details from the specified detail URL suffix. 85 86 Args: 87 client (Client): The client object used to make the request. 88 detail_url (str): The URL for fetching the attendance details. 89 90 Returns: 91 Dict[str, str]: A dictionary containing the attendance details. 92 93 Raises: 94 ParseError: If there is an error parsing the attendance details. 95 """ 96 details = {} 97 div = no_access_check( 98 BeautifulSoup( 99 client.get(client.ATTENDANCE_DETAILS_URL + detail_url).text, "lxml" 100 ) 101 ).find("div", attrs={"class": "container-background"}) 102 if div is None or isinstance(div, NavigableString): 103 raise ParseError("Error in parsing attendance details") 104 line = div.find_all("tr", attrs={"class": ["line0", "line1"]}) 105 if len(line) < 1: 106 raise ParseError("Error in parsing attendance details (Lines are empty).") 107 for l in line: 108 th = l.find("th") 109 td = l.find("td") 110 if th is None or td is None: 111 continue 112 details[l.find("th").text] = l.find("td").text 113 return details
Retrieves attendance details from the specified detail URL suffix.
Args: client (Client): The client object used to make the request. detail_url (str): The URL for fetching the attendance details.
Returns: Dict[str, str]: A dictionary containing the attendance details.
Raises: ParseError: If there is an error parsing the attendance details.
198def get_subject_frequency( 199 client: Client, 200 attendances=None, 201 start: Optional[datetime] = None, 202 end: Optional[datetime] = None, 203) -> Dict[str, float]: 204 if not attendances: 205 attendances = asyncio.run(_get_subject_attendance(client, start, end)) 206 frequency = {} 207 for sub in attendances: 208 attended = attendances[sub].get("ob", 0) + attendances[sub].get("sp", 0) 209 unattended = ( 210 attendances[sub].get("nb", 0) 211 + attendances[sub].get("u", 0) 212 + attendances[sub].get("zw", 0) 213 ) 214 total = attended + unattended 215 frequency[sub] = round(attended / total * 100, 2) if total > 0 else 100.0 216 return frequency
219def get_gateway_attendance(client: Client) -> List[Tuple[Tuple[str, str], str, str]]: 220 """ 221 Retrieves attendance data from the gateway API. 222 223 The gateway API data is typically updated every 3 hours. 224 Accessing api.librus.pl requires a private key. 225 226 Requires: 227 oauth token to be refreshed with client.refresh_oauth() 228 229 Args: 230 client (Client): The client object used to make the request. 231 232 Returns: 233 List[Tuple[Tuple[str, str], str, str]]: A list of tuples containing attendance data. 234 Each tuple contains three elements: 235 1. Tuple containing type abbreviation and type name. 236 2. Lesson number. 237 3. Semester. 238 239 Raises: 240 ValueError: If the OAuth token is missing. 241 AuthorizationError: If there is an authorization error while accessing the API. 242 """ 243 types = { 244 "1": {"short": "nb", "name": "Nieobecność"}, 245 "2": {"short": "sp", "name": "Spóźnienie"}, 246 "3": {"short": "u", "name": "Nieobecność uspr."}, 247 "4": {"short": "zw", "name": "Zwolnienie"}, 248 "100": {"short": "ob", "name": "Obecność"}, 249 "1266": {"short": "wy", "name": "Wycieczka"}, 250 "2022": {"short": "k", "name": "Konkurs szkolny"}, 251 "2829": {"short": "sz", "name": "Szkolenie"}, 252 } 253 oauth = client.token.oauth 254 if oauth == "": 255 oauth = client.refresh_oauth() 256 client.cookies["oauth_token"] = oauth 257 response = client.get(client.GATEWAY_API_ATTENDANCE) 258 259 attendances = response.json()["Attendances"] 260 _attendance = [] 261 for a in attendances: 262 type_id = a["Type"]["Id"] 263 type_data = tuple(types[str(type_id)].values()) 264 lesson_number = a["LessonNo"] 265 semester = a["Semester"] 266 267 _attendance.append((type_data, lesson_number, semester)) 268 269 return _attendance
Retrieves attendance data from the gateway API.
The gateway API data is typically updated every 3 hours. Accessing api.librus.pl requires a private key.
Requires: oauth token to be refreshed with client.refresh_oauth()
Args: client (Client): The client object used to make the request.
Returns: List[Tuple[Tuple[str, str], str, str]]: A list of tuples containing attendance data. Each tuple contains three elements: 1. Tuple containing type abbreviation and type name. 2. Lesson number. 3. Semester.
Raises: ValueError: If the OAuth token is missing. AuthorizationError: If there is an authorization error while accessing the API.
272def get_attendance_frequency(client: Client) -> Tuple[float, float, float]: 273 """ 274 Calculates the attendance frequency for each semester and overall. 275 276 Args: 277 client (Client): The client object used to retrieve attendance data. 278 279 Returns: 280 Tuple[float, float, float]: A tuple containing the attendance frequencies for the first semester, second semester, and overall. 281 Each frequency is a float value between 0 and 1, representing the ratio of attended lessons to total lessons. 282 283 Raises: 284 ValueError: If there is an error retrieving attendance data. 285 """ 286 attendance = get_gateway_attendance(client) 287 first_semester = [a for a in attendance if a[2] == 1] 288 second_semester = [a for a in attendance if a[2] == 2] 289 f_attended = len([a for a in first_semester if a[0][0] in ["wy", "ob", "sp"]]) 290 s_attended = len([a for a in second_semester if a[0][0] in ["wy", "ob", "sp"]]) 291 f_freq = f_attended / len(first_semester) if len(first_semester) != 0 else 1 292 s_freq = s_attended / len(second_semester) if len(second_semester) != 0 else 1 293 overall_freq = ( 294 len([a for a in attendance if a[0][0] in ["wy", "ob", "sp"]]) / len(attendance) 295 if len(attendance) != 0 296 else 1 297 ) 298 return f_freq, s_freq, overall_freq 299 # ADD Lesson frequency
Calculates the attendance frequency for each semester and overall.
Args: client (Client): The client object used to retrieve attendance data.
Returns: Tuple[float, float, float]: A tuple containing the attendance frequencies for the first semester, second semester, and overall. Each frequency is a float value between 0 and 1, representing the ratio of attended lessons to total lessons.
Raises: ValueError: If there is an error retrieving attendance data.
376def get_attendance(client: Client, sort_by: str = "all") -> List[List[Attendance]]: 377 """ 378 Retrieves attendance records from librus. 379 380 Args: 381 client (Client): The client object used to fetch attendance data. 382 sort_by (str, optional): The sorting criteria for attendance records. 383 It can be one of the following values: 384 - "all": Sort by all attendance records. 385 - "week": Sort by attendance records for the current week. 386 - "last_login": Sort by attendance records since the last login. 387 Defaults to "all". 388 389 Returns: 390 List[List[Attendance]]: A list containing attendance records grouped by semester. 391 Each inner list represents attendance records for a specific semester. 392 393 Raises: 394 ArgumentError: If an invalid value is provided for the sort_by parameter. 395 ParseError: If there is an error parsing the attendance data. 396 """ 397 SORT: Dict[str, Dict[str, str]] = { 398 "all": {"zmiany_logowanie_wszystkie": ""}, 399 "week": {"zmiany_logowanie_tydzien": "zmiany_logowanie_tydzien"}, 400 "last_login": {"zmiany_logowanie": "zmiany_logowanie"}, 401 } 402 if sort_by not in SORT.keys(): 403 raise ArgumentError( 404 "Wrong value for sort_by it can be either all, week or last_login" 405 ) 406 407 soup = no_access_check( 408 BeautifulSoup( 409 client.post(client.ATTENDANCE_URL, data=SORT[sort_by]).text, 410 "lxml", 411 ) 412 ) 413 table = soup.find("table", attrs={"class": "center big decorated"}) 414 if table is None or isinstance(table, NavigableString): 415 raise ParseError("Error parsing attendance (table).") 416 417 days = table.find_all("tr", attrs={"class": ["line0", "line1"]}) 418 attendance_semesters = [[] for _ in range(2)] # Two semesters 419 semester = -1 420 for day in days: 421 if day.find("td", attrs={"class": "center bolded"}): 422 # marker to increment semester 423 semester += 1 424 attendance = day.find_all("td", attrs={"class": "center"}) 425 for absence in attendance: 426 a_elem: List[Tag] = absence.find_all("a") 427 for single in a_elem: 428 attendance_semesters[semester].append( 429 _create_attendance(single, semester) 430 ) 431 match semester: 432 case 0: 433 return list(attendance_semesters) 434 case 1: 435 return list(reversed(attendance_semesters)) 436 case _: 437 raise ParseError("Couldn't find attendance semester")
Retrieves attendance records from librus.
Args: client (Client): The client object used to fetch attendance data. sort_by (str, optional): The sorting criteria for attendance records. It can be one of the following values: - "all": Sort by all attendance records. - "week": Sort by attendance records for the current week. - "last_login": Sort by attendance records since the last login. Defaults to "all".
Returns: List[List[Attendance]]: A list containing attendance records grouped by semester. Each inner list represents attendance records for a specific semester.
Raises: ArgumentError: If an invalid value is provided for the sort_by parameter. ParseError: If there is an error parsing the attendance data.