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

Attendance( symbol: str, href: str, semester: int, date: str, type: str, teacher: str, period: int, excursion: bool, topic: str, subject: str)
symbol: str
href: str
semester: int
date: str
type: str
teacher: str
period: int
excursion: bool
topic: str
subject: str
def get_detail(client: librus_apix.client.Client, detail_url: str) -> Dict[str, str]:
 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.

def get_subject_frequency(client: librus_apix.client.Client) -> Dict[str, float]:
168def get_subject_frequency(client: Client) -> Dict[str, float]:
169    res = _get_subject_attendance(client)
170    frequency = {}
171    for sub in res:
172        attended = res[sub].get("ob", 0) + res[sub].get("sp", 0)
173        unattended = (
174            res[sub].get("nb", 0) + res[sub].get("u", 0) + res[sub].get("zw", 0)
175        )
176        total = attended + unattended
177        frequency[sub] = round(attended/total*100, 2) if total > 0 else 100.0
178    return frequency
def get_gateway_attendance( client: librus_apix.client.Client) -> List[Tuple[Tuple[str, str], str, str]]:
181def get_gateway_attendance(client: Client) -> List[Tuple[Tuple[str, str], str, str]]:
182    """
183    Retrieves attendance data from the gateway API.
184
185    The gateway API data is typically updated every 3 hours.
186    Accessing api.librus.pl requires a private key.
187
188    Requires:
189        oauth token to be refreshed with client.refresh_oauth()
190
191    Args:
192        client (Client): The client object used to make the request.
193
194    Returns:
195        List[Tuple[Tuple[str, str], str, str]]: A list of tuples containing attendance data.
196            Each tuple contains three elements:
197            1. Tuple containing type abbreviation and type name.
198            2. Lesson number.
199            3. Semester.
200
201    Raises:
202        ValueError: If the OAuth token is missing.
203        AuthorizationError: If there is an authorization error while accessing the API.
204    """
205    types = {
206        "1": {"short": "nb", "name": "Nieobecność"},
207        "2": {"short": "sp", "name": "Spóźnienie"},
208        "3": {"short": "u", "name": "Nieobecność uspr."},
209        "4": {"short": "zw", "name": "Zwolnienie"},
210        "100": {"short": "ob", "name": "Obecność"},
211        "1266": {"short": "wy", "name": "Wycieczka"},
212        "2022": {"short": "k", "name": "Konkurs szkolny"},
213        "2829": {"short": "sz", "name": "Szkolenie"},
214    }
215    oauth = client.token.oauth
216    if oauth == "":
217        oauth = client.refresh_oauth()
218    client.cookies["oauth_token"] = oauth
219    response = client.get(client.GATEWAY_API_ATTENDANCE)
220
221    attendances = response.json()["Attendances"]
222    _attendance = []
223    for a in attendances:
224        type_id = a["Type"]["Id"]
225        type_data = tuple(types[str(type_id)].values())
226        lesson_number = a["LessonNo"]
227        semester = a["Semester"]
228
229        _attendance.append((type_data, lesson_number, semester))
230
231    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.

def get_attendance_frequency(client: librus_apix.client.Client) -> Tuple[float, float, float]:
234def get_attendance_frequency(client: Client) -> Tuple[float, float, float]:
235    """
236    Calculates the attendance frequency for each semester and overall.
237
238    Args:
239        client (Client): The client object used to retrieve attendance data.
240
241    Returns:
242        Tuple[float, float, float]: A tuple containing the attendance frequencies for the first semester, second semester, and overall.
243            Each frequency is a float value between 0 and 1, representing the ratio of attended lessons to total lessons.
244
245    Raises:
246        ValueError: If there is an error retrieving attendance data.
247    """
248    attendance = get_gateway_attendance(client)
249    first_semester = [a for a in attendance if a[2] == 1]
250    second_semester = [a for a in attendance if a[2] == 2]
251    f_attended = len([a for a in first_semester if a[0][0] in ["wy", "ob", "sp"]])
252    s_attended = len([a for a in second_semester if a[0][0] in ["wy", "ob", "sp"]])
253    f_freq = f_attended / len(first_semester) if len(first_semester) != 0 else 1
254    s_freq = s_attended / len(second_semester) if len(second_semester) != 0 else 1
255    overall_freq = (
256        len([a for a in attendance if a[0][0] in ["wy", "ob", "sp"]]) / len(attendance)
257        if len(attendance) != 0
258        else 1
259    )
260    return f_freq, s_freq, overall_freq
261    # 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.

def get_attendance( client: librus_apix.client.Client, sort_by: str = 'all') -> List[List[Attendance]]:
338def get_attendance(client: Client, sort_by: str = "all") -> List[List[Attendance]]:
339    """
340    Retrieves attendance records from librus.
341
342    Args:
343        client (Client): The client object used to fetch attendance data.
344        sort_by (str, optional): The sorting criteria for attendance records.
345            It can be one of the following values:
346            - "all": Sort by all attendance records.
347            - "week": Sort by attendance records for the current week.
348            - "last_login": Sort by attendance records since the last login.
349            Defaults to "all".
350
351    Returns:
352        List[List[Attendance]]: A list containing attendance records grouped by semester.
353            Each inner list represents attendance records for a specific semester.
354
355    Raises:
356        ArgumentError: If an invalid value is provided for the sort_by parameter.
357        ParseError: If there is an error parsing the attendance data.
358    """
359    SORT: Dict[str, Dict[str, str]] = {
360        "all": {"zmiany_logowanie_wszystkie": ""},
361        "week": {"zmiany_logowanie_tydzien": "zmiany_logowanie_tydzien"},
362        "last_login": {"zmiany_logowanie": "zmiany_logowanie"},
363    }
364    if sort_by not in SORT.keys():
365        raise ArgumentError(
366            "Wrong value for sort_by it can be either all, week or last_login"
367        )
368
369    soup = no_access_check(
370        BeautifulSoup(
371            client.post(client.ATTENDANCE_URL, data=SORT[sort_by]).text,
372            "lxml",
373        )
374    )
375    table = soup.find("table", attrs={"class": "center big decorated"})
376    if table is None or isinstance(table, NavigableString):
377        raise ParseError("Error parsing attendance (table).")
378
379    days = table.find_all("tr", attrs={"class": ["line0", "line1"]})
380    attendance_semesters = [[] for _ in range(2)]  # Two semesters
381    semester = -1
382    for day in days:
383        if day.find("td", attrs={"class": "center bolded"}):
384            # marker to increment semester
385            semester += 1
386        attendance = day.find_all("td", attrs={"class": "center"})
387        for absence in attendance:
388            a_elem: List[Tag] = absence.find_all("a")
389            for single in a_elem:
390                attendance_semesters[semester].append(
391                    _create_attendance(single, semester)
392                )
393    match semester:
394        case 0:
395            return list(attendance_semesters)
396        case 1:
397            return list(reversed(attendance_semesters))
398        case _:
399            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.