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