Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions edupage_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
from edupage_api.substitution import Substitution, TimetableChange
from edupage_api.timeline import TimelineEvent, TimelineEvents
from edupage_api.timetables import Timetable, Timetables
from edupage_api.attendance import (
Attendance,
AttendanceStatistic,
AttendenceStatDetail,
Arrival,
)


class Edupage(EdupageModule):
Expand Down Expand Up @@ -136,6 +142,54 @@ def get_subjects(self) -> Optional[list[Subject]]:

return Subjects(self).get_subjects()

def get_attendance_statistics(
self, user_id: str, date: date
) -> AttendanceStatistic:
"""Get a count of excused/unexcused/absent/late (and more) lessons.

Note: To get your own attendance, pass `edupage.get_user_id()` as `user_id`

Args:
user_id (str): This user's attendance data will be in the result.
date (datetime.date): Date from which you want attendance statistics.

Returns:
AttendanceStatistic: The fetched statistics
"""

return Attendance(self).get_attendance_statistics(user_id, date)

def get_days_with_available_attendance(self, user_id: str) -> list[date]:
"""Get all dates where attendance data is available.

Note: To get your data for your own attendance, pass `edupage.get_user_id()` as `user_id`

Args:
user_id (str): This user's available attendence dates will be in the result.

Result:
list[datetime.date]: A list of all dates where attendence data is available.
"""

return Attendance(self).get_days_with_available_attendance(user_id)

def get_arrivals(self, user_id: str) -> dict[str, Arrival]:
"""Get arrival and departure times for all days returned by `Edupage.get_days_with_available_attendance`.

Note: To get your own arrivals, pass `edupage.get_user_id()` as `user_id`
Note: Only returns data when available. There may not be an entry for each day returned by `Edupage.get_days_with_available_attendance`.

Args:
user_id (str): This user's arrivals dates will be in the result.

Result:
dict[str, Arrival]: The keys of the dict are dates of the arrival in "YYYY-mm-dd" format (%Y-%m-%d). Contains all available arrival and departure data.

Note: You don't have to rely on the keys of the dictionary, there is also a `Arrival.date` property.
"""

return Attendance(self).get_arrivals(user_id)

def send_message(
self, recipients: Union[list[EduAccount], EduAccount], body: str
) -> int:
Expand Down
248 changes: 248 additions & 0 deletions edupage_api/attendance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import json

from dataclasses import dataclass
from datetime import date, datetime
from typing import Optional

from edupage_api.module import Module, ModuleHelper
from edupage_api.exceptions import (
MissingDataException,
InsufficientPermissionsException,
)


@dataclass
class AttendenceStatDetail:
count: float
excused: float
unexcused: float


@dataclass
class AttendanceStatistic:
total_lessons_absent: AttendenceStatDetail
total_late_minutes: AttendenceStatDetail
total_early_minutes: AttendenceStatDetail

theory_lessons_total: AttendenceStatDetail
theory_lessons_absent: AttendenceStatDetail
theory_late_minutes: AttendenceStatDetail
theory_early_minutes: AttendenceStatDetail

training_lessons_total: AttendenceStatDetail
training_lessons_absent: AttendenceStatDetail
training_late_minutes: AttendenceStatDetail
training_early_minutes: AttendenceStatDetail

late_count: float
early_count: float
present_lessons_count: float
distant_lessons_count: float

# TODO (in the future): fields: "todo", "error", "sa_dontcount" - what do they mean?


@dataclass
class Arrival:
date: date
arrival: Optional[datetime]
departure: Optional[datetime]


class Attendance(Module):
def __get_attendance_data(self, user_id: str):
request_url = f"https://{self.edupage.subdomain}.edupage.org/dashboard/eb.php"
params = {
"ebuid": user_id,
"mode": "attendance",
}

response = self.edupage.session.get(request_url, params=params)
response_html = response.text

user_id_number = Attendance.__get_user_id_number(user_id)
try:
data = response_html.split(
'ASC.requireAsync("/dashboard/dochadzka.js#initZiak").then'
)[1][37:].split(f",[{user_id_number}],true);")[0]

return json.loads(data)
except IndexError:
raise MissingDataException(
'Unexpected response from attendance endpoint! (expected string `ASC.requireAsync("/dashboard/dochadzka.js#initZiak").then` to be in the response)'
)

@staticmethod
def __get_user_id_number(user_id: str):
return user_id.replace("Student", "").replace("Ucitel", "").replace("-", "")

def get_days_with_available_attendance(self, user_id: str) -> list[date]:
user_id_number = Attendance.__get_user_id_number(
self.edupage.get_user_id() # pyright: ignore[reportAttributeAccessIssue]
)

target_user_id_number = Attendance.__get_user_id_number(user_id)
attendance_data = self.__get_attendance_data(user_id_number)

stats = attendance_data["dateStats"]
if target_user_id_number not in stats:
raise InsufficientPermissionsException(
"The requested user's data is not available in the response."
)

return [
datetime.strptime("%Y-%m-%d", d).date()
for d in stats[target_user_id_number].keys()
]

@ModuleHelper.logged_in
def get_arrivals(self, user_id: str) -> dict[str, Arrival]:
user_id_number = Attendance.__get_user_id_number(
self.edupage.get_user_id() # pyright: ignore[reportAttributeAccessIssue]
)

target_user_id_number = Attendance.__get_user_id_number(user_id)

attendance_data = self.__get_attendance_data(user_id_number)
detailed_stats = attendance_data["students"].get(target_user_id_number)
if detailed_stats is None:
raise InsufficientPermissionsException(
"The requested user's data is not available in the response."
)

arrivals = {}
for raw_date, arrival_data in detailed_stats.items():
if "prichod" not in arrival_data and "odchod" not in arrival_data:
continue

parsed_date = datetime.strptime(raw_date, "%Y-%m-%d").date()

arrival_time = (
datetime.strptime(
arrival_data.get("prichod"),
"%H:%M:%S",
).replace(
year=parsed_date.year, month=parsed_date.month, day=parsed_date.day
)
if arrival_data.get("prichod") is not None
else None
)

departure_time = (
datetime.strptime(
arrival_data.get("odchod"),
"%H:%M:%S",
).replace(
year=parsed_date.year, month=parsed_date.month, day=parsed_date.day
)
if arrival_data.get("odchod") is not None
else None
)

arrivals[raw_date] = Arrival(parsed_date, arrival_time, departure_time)

return arrivals

@ModuleHelper.logged_in
def get_attendance_statistics(self, user_id: str, date: date):
user_id_number = Attendance.__get_user_id_number(
self.edupage.get_user_id() # pyright: ignore[reportAttributeAccessIssue]
)

target_user_id_number = Attendance.__get_user_id_number(user_id)

attendance_data = self.__get_attendance_data(user_id_number)

all_stats = attendance_data["dateStats"]

user_stats = all_stats.get(target_user_id_number)
if user_stats is None:
raise InsufficientPermissionsException(
"The requested user's data is not available in the response."
)

stats = user_stats[date.strftime("%Y-%m-%d")]

total_lessons_absent = AttendenceStatDetail(
count=stats.get("absent"),
excused=stats.get("excused"),
unexcused=stats.get("unexcused"),
)

total_late_minutes = AttendenceStatDetail(
count=stats.get("late_minutes"),
excused=stats.get("late_excused_auto"),
unexcused=stats.get("late_unexcused_auto"),
)

total_early_minutes = AttendenceStatDetail(
count=stats.get("early_minutes"),
excused=stats.get("early_excused_auto"),
unexcused=stats.get("early_unexcused_auto"),
)

theory_lessons_total = AttendenceStatDetail(
count=stats.get("teoria_absent"),
excused=stats.get("teoria_excused"),
unexcused=stats.get("teoria_unexcused"),
)

theory_lessons_absent = AttendenceStatDetail(
count=stats.get("teoria_absent_only"),
excused=stats.get("teoria_absent_excused"),
unexcused=stats.get("teoria_absent_unexcused"),
)

theory_early_minutes = AttendenceStatDetail(
count=stats.get("teoria_early"),
excused=stats.get("teoria_early_excused"),
unexcused=stats.get("teoria_early_unexcused"),
)

theory_late_minutes = AttendenceStatDetail(
count=stats.get("teoria_late"),
excused=stats.get("teoria_late_excused"),
unexcused=stats.get("teoria_late_unexcused"),
)

training_lessons_total = AttendenceStatDetail(
count=stats.get("prax_absent"),
excused=stats.get("prax_excused"),
unexcused=stats.get("prax_unexcused"),
)

training_lessons_absent = AttendenceStatDetail(
count=stats.get("prax_absent_only"),
excused=stats.get("prax_absent_excused"),
unexcused=stats.get("prax_absent_unexcused"),
)

training_early_minutes = AttendenceStatDetail(
count=stats.get("prax_early"),
excused=stats.get("prax_early_excused"),
unexcused=stats.get("prax_early_unexcused"),
)

training_late_minutes = AttendenceStatDetail(
count=stats.get("prax_late"),
excused=stats.get("prax_late_excused"),
unexcused=stats.get("prax_late_unexcused"),
)

return AttendanceStatistic(
total_lessons_absent,
total_late_minutes,
total_early_minutes,
theory_lessons_total,
theory_lessons_absent,
theory_late_minutes,
theory_early_minutes,
training_lessons_total,
training_lessons_absent,
training_late_minutes,
training_early_minutes,
late_count=stats.get("late"),
early_count=stats.get("early"),
present_lessons_count=stats.get("present"),
distant_lessons_count=stats.get("distant"),
)