diff --git a/edupage_api/__init__.py b/edupage_api/__init__.py index 90f9493..20f5676 100644 --- a/edupage_api/__init__.py +++ b/edupage_api/__init__.py @@ -14,13 +14,13 @@ from edupage_api.lunches import Lunch, Lunches from edupage_api.messages import Messages from edupage_api.module import EdupageModule +from edupage_api.parent import Parent from edupage_api.people import (EduAccount, EduStudent, EduStudentSkeleton, EduTeacher, People) from edupage_api.ringing import RingingTime, RingingTimes from edupage_api.substitution import Substitution, TimetableChange from edupage_api.timeline import TimelineEvent, TimelineEvents from edupage_api.timetables import Timetable, Timetables -from edupage_api.parent import Parent class Edupage(EdupageModule): @@ -40,7 +40,9 @@ def __init__(self, request_timeout=5): self.username = None self.session = requests.session() - self.session.request = functools.partial(self.session.request, timeout=request_timeout) + self.session.request = functools.partial( + self.session.request, timeout=request_timeout + ) def login( self, username: str, password: str, subdomain: str @@ -100,13 +102,15 @@ def get_teachers(self) -> Optional[list[EduTeacher]]: return People(self).get_teachers() - def send_message(self, recipients: Union[list[EduAccount], EduAccount], body: str) -> int: + def send_message( + self, recipients: Union[list[EduAccount], EduAccount], body: str + ) -> int: """Send message. Args: recipients (Optional[list[EduAccount]]): Recipients of your message (list of `EduAccount`s). body (str): Body of your message. - + Returns: int: The timeline id of the new message. """ @@ -166,10 +170,10 @@ def get_grades(self) -> list[EduGrade]: """ return Grades(self).get_grades(year=None, term=None) - + def get_grades_for_term(self, year: int, term: Term) -> list[EduGrade]: """Get a list of all available grades for a given year and term - + Returns: list[EduGrade]: List of `EduGrade`s """ @@ -185,7 +189,9 @@ def get_user_id(self) -> str: return self.data.get("userid") - def custom_request(self, url: str, method: str, data: str = "", headers: dict = {}) -> Response: + def custom_request( + self, url: str, method: str, data: str = "", headers: dict = {} + ) -> Response: """Send custom request to EduPage. Args: @@ -255,17 +261,17 @@ def get_next_ringing_time(self, date_time: datetime) -> RingingTime: RingingTime: The type (break or lesson) and time of the next ringing. """ return RingingTimes(self).get_next_ringing_time(date_time) - + def switch_to_child(self, child: Union[EduAccount, int]): """Switch to an account of a child - can only be used on parent accounts Args: child (EduAccount | int): The account or `person_id` of the child you want to switch to - Note: When you switch to a child account, all other methods will return data as if you were logged in as `child` + Note: When you switch to a child account, all other methods will return data as if you were logged in as `child` """ Parent(self).switch_to_child(child) - + def switch_to_parent(self): """Switches back to your parent account - can only be used on parent accounts""" Parent(self).switch_to_parent() @@ -285,4 +291,4 @@ def from_session_id(cls, session_id: str, subdomain: str): Login(instance).reload_data(subdomain, session_id) - return instance \ No newline at end of file + return instance diff --git a/edupage_api/cloud.py b/edupage_api/cloud.py index a119978..93bb156 100644 --- a/edupage_api/cloud.py +++ b/edupage_api/cloud.py @@ -42,7 +42,7 @@ def parse(data: dict): data.get("extension"), data.get("type"), data.get("file"), - data.get("name") + data.get("name"), ) @@ -77,7 +77,9 @@ def upload_file(self, fd: TextIOWrapper) -> EduCloudFile: EduCloudFile: `EduCloudFile` object. """ - request_url = f"https://{self.edupage.subdomain}.edupage.org/timeline/?akcia=uploadAtt" + request_url = ( + f"https://{self.edupage.subdomain}.edupage.org/timeline/?akcia=uploadAtt" + ) files = {"att": fd} diff --git a/edupage_api/compression.py b/edupage_api/compression.py index fc39a1d..ac6ebca 100644 --- a/edupage_api/compression.py +++ b/edupage_api/compression.py @@ -1,24 +1,20 @@ import zlib from hashlib import sha1 from typing import Union -from edupage_api.exceptions import Base64DecodeError +from edupage_api.exceptions import Base64DecodeError from edupage_api.module import ModuleHelper + # compression parameters from https://github.com/rgnter/epea_cpp # encoding and decoding from https://github.com/jsdom/abab class RequestData: @staticmethod def __compress(data: bytes) -> bytes: compressor = zlib.compressobj( - -1, - zlib.DEFLATED, - -15, - 8, - zlib.Z_DEFAULT_STRATEGY + -1, zlib.DEFLATED, -15, 8, zlib.Z_DEFAULT_STRATEGY ) - compressor.compress(data) return compressor.flush(zlib.Z_FINISH) @@ -36,10 +32,11 @@ def chromium_base64_encode(data: str) -> str: # Lookup table for btoa(), which converts a six-bit number into the # corresponding ASCII character. chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + def btoa_lookup(index): if index >= 0 and index < 64: return chars[index] - + return None out = "" @@ -50,28 +47,26 @@ def btoa_lookup(index): if length > i + 1: groups_of_six[1] |= ord(data[i + 1]) >> 4 - groups_of_six[2] = (ord(data[i + 1]) & 0x0f) << 2 + groups_of_six[2] = (ord(data[i + 1]) & 0x0F) << 2 if length > i + 2: groups_of_six[2] |= ord(data[i + 2]) >> 6 - groups_of_six[3] = ord(data[i + 2]) & 0x3f + groups_of_six[3] = ord(data[i + 2]) & 0x3F for k in groups_of_six: if k is None: out += "=" else: out += btoa_lookup(k) - - + i += 3 return out - def chromium_base64_decode(data: str) -> str: # "Remove all ASCII whitespace from data." [data := data.replace(char, "") for char in "\t\n\f\r"] - + # "If data's code point length divides by 4 leaving no remainder, then: if data ends # with one or two U+003D (=) code points, then remove them from data." if len(data) % 4 == 0: @@ -79,8 +74,11 @@ def chromium_base64_decode(data: str) -> str: data = data.replace("==", "") elif data.endswith("="): data = data.replace("=", "") - - allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + + allowed_chars = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + ) + def atob_lookup(ch: str): try: return allowed_chars.index(ch) @@ -100,7 +98,7 @@ def atob_lookup(ch: str): data_contains_invalid_chars = False in [ch in allowed_chars for ch in data] if len(data) % 4 == 1 or data_contains_invalid_chars: return None - + # "Let output be an empty byte sequence." output = "" @@ -110,7 +108,7 @@ def atob_lookup(ch: str): # when we've gotten to 24 bits. buffer = 0 accumulated_bits = 0 - + # "Let position be a position variable for data, initially pointing at the # start of data." # @@ -132,13 +130,13 @@ def atob_lookup(ch: str): # big-endian numbers. Append three bytes with values equal to those # numbers to output, in the same order, and then empty buffer." if accumulated_bits == 24: - output += chr((buffer & 0xff0000) >> 16) - output += chr((buffer & 0xff00) >> 8) - output += chr(buffer & 0xff) - + output += chr((buffer & 0xFF0000) >> 16) + output += chr((buffer & 0xFF00) >> 8) + output += chr(buffer & 0xFF) + buffer = 0 accumulated_bits = 0 - + # "If buffer is not empty, it contains either 12 or 18 bits. If it contains # 12 bits, then discard the last four and interpret the remaining eight as # an 8-bit big-endian number. If it contains 18 bits, then discard the last @@ -150,49 +148,55 @@ def atob_lookup(ch: str): output += chr(buffer) elif accumulated_bits == 18: buffer >>= 2 - output += chr((buffer & 0xff00) >> 8) - output += chr(buffer & 0xff) - + output += chr((buffer & 0xFF00) >> 8) + output += chr(buffer & 0xFF) + return output - @staticmethod def __encode_data(data: str) -> bytes: compressed = RequestData.__compress(data.encode()) - encoded = RequestData.chromium_base64_encode("".join([chr(ch) for ch in compressed])) + encoded = RequestData.chromium_base64_encode( + "".join([chr(ch) for ch in compressed]) + ) return encoded - + @staticmethod def __decode_data(data: str) -> str: return RequestData.chromium_base64_decode(data) @staticmethod def encode_request_body(request_data: Union[dict, str]) -> str: - encoded_data = ModuleHelper.encode_form_data(request_data) if type(request_data) == dict else request_data + encoded_data = ( + ModuleHelper.encode_form_data(request_data) + if type(request_data) == dict + else request_data + ) encoded_data = RequestData.__encode_data(encoded_data) data_hash = sha1(encoded_data.encode()).hexdigest() - return ModuleHelper.encode_form_data({ - "eqap": f"dz:{encoded_data}", - "eqacs": data_hash, - "eqaz": "1" # use "encryption"? (compression) - }) - + return ModuleHelper.encode_form_data( + { + "eqap": f"dz:{encoded_data}", + "eqacs": data_hash, + "eqaz": "1", # use "encryption"? (compression) + } + ) + @staticmethod def decode_response(response: str) -> str: # error if response.startswith("eqwd:"): return RequestData.chromium_base64_decode(response[5:]) - + # response not compressed if not response.startswith("eqz:"): return response - decoded = RequestData.__decode_data(response[4:]) if decoded is None: raise Base64DecodeError("Failed to decode response.") - - return decoded \ No newline at end of file + + return decoded diff --git a/edupage_api/custom_request.py b/edupage_api/custom_request.py index a40c3c6..2d4c5b9 100644 --- a/edupage_api/custom_request.py +++ b/edupage_api/custom_request.py @@ -4,7 +4,9 @@ class CustomRequest(Module): - def custom_request(self, url: str, method: str, data: str = "", headers: dict = {}) -> Response: + def custom_request( + self, url: str, method: str, data: str = "", headers: dict = {} + ) -> Response: if method == "GET": response = self.edupage.session.get(url, headers=headers) elif method == "POST": diff --git a/edupage_api/dbi.py b/edupage_api/dbi.py index 4a9db48..c3b45c2 100644 --- a/edupage_api/dbi.py +++ b/edupage_api/dbi.py @@ -102,4 +102,6 @@ def fetch_person_data_by_name(self, name: str) -> Optional[dict]: student_data = self.fetch_student_data_by_name(name) parent_data = self.fetch_parent_data_by_name(name) - return ModuleHelper.return_first_not_null(teacher_data, student_data, parent_data) + return ModuleHelper.return_first_not_null( + teacher_data, student_data, parent_data + ) diff --git a/edupage_api/exceptions.py b/edupage_api/exceptions.py index 7df2d18..9fb1227 100644 --- a/edupage_api/exceptions.py +++ b/edupage_api/exceptions.py @@ -45,20 +45,26 @@ class RequestError(Exception): class InvalidLunchData(Exception): pass + class Base64DecodeError(Exception): pass + class InvalidRecipientsException(Exception): pass + class InvalidChildException(Exception): pass + class UnknownServerError(Exception): pass + class NotParentException(Exception): pass + class SecondFactorFailedException(Exception): - pass \ No newline at end of file + pass diff --git a/edupage_api/foreign_timetables.py b/edupage_api/foreign_timetables.py index 1777972..d8d7850 100644 --- a/edupage_api/foreign_timetables.py +++ b/edupage_api/foreign_timetables.py @@ -51,16 +51,20 @@ def __get_timetable_data(self, id: int, table: str, date: datetime): "showColors": True, "showIgroupsInClasses": True, "showOrig": True, - "log_module": "CurrentTTView" - } + "log_module": "CurrentTTView", + }, ], - "__gsh": self.edupage.gsec_hash + "__gsh": self.edupage.gsec_hash, } - request_url = (f"https://{self.edupage.subdomain}.edupage.org/" - "timetable/server/currenttt.js?__func=curentttGetData") + request_url = ( + f"https://{self.edupage.subdomain}.edupage.org/" + "timetable/server/currenttt.js?__func=curentttGetData" + ) - timetable_data = self.edupage.session.post(request_url, json=request_data).content.decode() + timetable_data = self.edupage.session.post( + request_url, json=request_data + ).content.decode() timetable_data = json.loads(timetable_data) timetable_data_response = timetable_data.get("r") @@ -70,7 +74,9 @@ def __get_timetable_data(self, id: int, table: str, date: datetime): raise MissingDataException("The server returned an incorrect response.") if timetable_data_error is not None: - raise RequestError(f"Edupage returned an error response: {timetable_data_error}") + raise RequestError( + f"Edupage returned an error response: {timetable_data_error}" + ) return timetable_data_response.get("ttitems") @@ -109,7 +115,9 @@ def classroom_by_id(id: str): table = "classrooms" if not table: - raise MissingDataException(f"Teacher, student or class with id {id} doesn't exist!") + raise MissingDataException( + f"Teacher, student or class with id {id} doesn't exist!" + ) timetable_data = self.__get_timetable_data(id, table, date) @@ -154,11 +162,23 @@ def classroom_by_id(id: str): classrooms = [classroom_by_id(id) for id in skeleton.get("classroomids")] - duration = (skeleton.get("durationperiods") - if skeleton.get("durationperiods") is not None else 1) - - new_skeleton = LessonSkeleton(date.weekday(), start_time, end_time, - subject_id, subject_name, classes, groups, classrooms, - duration, teachers) + duration = ( + skeleton.get("durationperiods") + if skeleton.get("durationperiods") is not None + else 1 + ) + + new_skeleton = LessonSkeleton( + date.weekday(), + start_time, + end_time, + subject_id, + subject_name, + classes, + groups, + classrooms, + duration, + teachers, + ) skeletons.append(new_skeleton) return skeletons diff --git a/edupage_api/grades.py b/edupage_api/grades.py index 679cd9a..7d71665 100644 --- a/edupage_api/grades.py +++ b/edupage_api/grades.py @@ -1,8 +1,8 @@ import json from dataclasses import dataclass from datetime import datetime -from typing import Optional, Union from enum import Enum +from typing import Optional, Union from edupage_api.dbi import DbiHelper from edupage_api.exceptions import FailedToParseGradeDataError @@ -25,6 +25,7 @@ class EduGrade: verbal: bool percent: float + class Term(Enum): FIRST = "P1" SECOND = "P2" @@ -32,8 +33,9 @@ class Term(Enum): class Grades(Module): def __parse_grade_data(self, data: str) -> dict: - json_string = data.split(".znamkyStudentViewer(")[1] \ - .split(");\r\n\t\t});\r\n\t\t")[0] + json_string = data.split(".znamkyStudentViewer(")[1].split( + ");\r\n\t\t});\r\n\t\t" + )[0] return json.loads(json_string) @@ -45,7 +47,7 @@ def __get_grade_data(self): return self.__parse_grade_data(response) except json.JSONDecodeError: raise FailedToParseGradeDataError("Failed to parse JSON") - + def __get_grade_data_for_term(self, term: Term, year: int): request_url = f"https://{self.edupage.subdomain}.edupage.org/znamky/?what=studentviewer&znamky_yearid={year}&nadobdobie={term.value}" response = self.edupage.session.post(request_url).content.decode() @@ -54,14 +56,14 @@ def __get_grade_data_for_term(self, term: Term, year: int): return self.__parse_grade_data(response) except json.JSONDecodeError: raise FailedToParseGradeDataError("Failed to parse JSON") - + @ModuleHelper.logged_in - def get_grades( - self, - term: Optional[Term], - year: Optional[int] - ) -> list[EduGrade]: - grade_data = self.__get_grade_data_for_term(term, year) if term and year else self.__get_grade_data() + def get_grades(self, term: Optional[Term], year: Optional[int]) -> list[EduGrade]: + grade_data = ( + self.__get_grade_data_for_term(term, year) + if term and year + else self.__get_grade_data() + ) grades = grade_data.get("vsetkyZnamky") grade_details = grade_data.get("vsetkyUdalosti").get("edupage") @@ -119,7 +121,7 @@ def get_grades( max_points = float(details.get("p_vaha_body")) importance = float(details.get("p_vaha")) / 20 - # Grade + # Grade grade_raw = grade.get("data").split(" (", 1) if grade_raw[0].isdigit(): grade_n = float(grade_raw[0]) @@ -144,8 +146,20 @@ def get_grades( except: verbal = True - grade = EduGrade(event_id, title, grade_n, comment, date, subject_id, - subject_name, teacher, max_points, importance, verbal, percent) + grade = EduGrade( + event_id, + title, + grade_n, + comment, + date, + subject_id, + subject_name, + teacher, + max_points, + importance, + verbal, + percent, + ) output.append(grade) - return output \ No newline at end of file + return output diff --git a/edupage_api/login.py b/edupage_api/login.py index 05dd580..829fd58 100644 --- a/edupage_api/login.py +++ b/edupage_api/login.py @@ -1,12 +1,15 @@ import json - +from dataclasses import dataclass from json import JSONDecodeError from typing import Optional -from dataclasses import dataclass + from requests import Response -from edupage_api.exceptions import BadCredentialsException, MissingDataException, SecondFactorFailedException, RequestError -from edupage_api.module import Module, EdupageModule +from edupage_api.exceptions import (BadCredentialsException, + MissingDataException, RequestError, + SecondFactorFailedException) +from edupage_api.module import EdupageModule, Module + @dataclass class TwoFactorLogin: @@ -33,48 +36,53 @@ def is_confirmed(self): if data.get("status") == "fail": return False elif data.get("status") != "ok": - raise MissingDataException(f"Invalid response from edupage's server!: {str(data)}") + raise MissingDataException( + f"Invalid response from edupage's server!: {str(data)}" + ) self.__code = data["data"] return True - + def resend_notifications(self): """Resends the confirmation notification to all devices.""" - + request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/twofactor?akcia=resendNotifs" response = self.__edupage.session.post(request_url) data = response.json() if data.get("status") != "ok": raise RequestError(f"Failed to resend notifications: {str(data)}") - - + def __finish(self, code: str): - request_url = f"https://{self.__edupage.subdomain}.edupage.org/login/edubarLogin.php" + request_url = ( + f"https://{self.__edupage.subdomain}.edupage.org/login/edubarLogin.php" + ) parameters = { "csrfauth": self.__csrf_token, "t2fasec": code, "2fNoSave": "y", "2fform": "1", "gu": self.__authentication_endpoint, - "au": self.__authentication_token + "au": self.__authentication_token, } response = self.__edupage.session.post(request_url, parameters) - + if "window.location = gu;" in response.text: - cookies = self.__edupage.session.cookies.get_dict(f"{self.__edupage.subdomain}.edupage.org") + cookies = self.__edupage.session.cookies.get_dict( + f"{self.__edupage.subdomain}.edupage.org" + ) Login(self.__edupage).reload_data( - self.__edupage.subdomain, - cookies["PHPSESSID"], - self.__edupage.username + self.__edupage.subdomain, cookies["PHPSESSID"], self.__edupage.username ) return - - raise SecondFactorFailedException(f"Second factor failed! (wrong/expired code? expired session?)") + + raise SecondFactorFailedException( + f"Second factor failed! (wrong/expired code? expired session?)" + ) def finish(self): """Finish the second factor authentication process. @@ -90,10 +98,12 @@ def finish(self): """ if self.__code is None: - raise BadCredentialsException("Not confirmed! (you can only call finish after `TwoFactorLogin.is_confirmed` has returned True)") - + raise BadCredentialsException( + "Not confirmed! (you can only call finish after `TwoFactorLogin.is_confirmed` has returned True)" + ) + self.__finish(self.__code) - + def finish_with_code(self, code: str): """Finish the second factor authentication process. This function should be used when email 2fa codes are used to confirm the login. If you are using a device to confirm the login, please use `TwoFactorLogin.finish`. @@ -105,7 +115,7 @@ def finish_with_code(self, code: str): SecondFactorFailedException: An invalid 2fa code was provided. """ self.__finish(code) - + class Login(Module): def __parse_login_data(self, data): @@ -121,25 +131,24 @@ def __parse_login_data(self, data): self.edupage.is_logged_in = True self.edupage.gsec_hash = data.split('ASC.gsechash="')[1].split('"')[0] - + def __second_factor(self): - request_url = f"https://{self.edupage.subdomain}.edupage.org/login/twofactor?sn=1" + request_url = ( + f"https://{self.edupage.subdomain}.edupage.org/login/twofactor?sn=1" + ) two_factor_response = self.edupage.session.get(request_url) - + data = two_factor_response.content.decode() - csrf_token = data.split("csrfauth\" value=\"")[1].split("\"")[0] + csrf_token = data.split('csrfauth" value="')[1].split('"')[0] - authentication_token = data.split("au\" value=\"")[1].split("\"")[0] - authentication_endpoint = data.split("gu\" value=\"")[1].split("\"")[0] + authentication_token = data.split('au" value="')[1].split('"')[0] + authentication_endpoint = data.split('gu" value="')[1].split('"')[0] return TwoFactorLogin( - authentication_endpoint, - authentication_token, - csrf_token, - self.edupage + authentication_endpoint, authentication_token, csrf_token, self.edupage ) - + def __login(self, username: str, password: str, subdomain: str) -> Response: request_url = f"https://{subdomain}.edupage.org/login/index.php" @@ -160,7 +169,7 @@ def __login(self, username: str, password: str, subdomain: str) -> Response: if "bad=1" in response.url: raise BadCredentialsException() - + return response def login( diff --git a/edupage_api/lunches.py b/edupage_api/lunches.py index dc872df..ae0e83c 100644 --- a/edupage_api/lunches.py +++ b/edupage_api/lunches.py @@ -32,7 +32,7 @@ def rate(self, edupage: EdupageModule, quantity: int, quality: int): "mysqlDate": self.__date, "jedlo_dna": "2", "kvalita": str(quality), - "mnozstvo": str(quantity) + "mnozstvo": str(quantity), } response = edupage.session.post(request_url, data=data) @@ -73,16 +73,14 @@ def __make_choice(self, edupage: EdupageModule, choice_str: str): boarder_menu = { "stravnikid": self.__boarder_id, "mysqlDate": self.date.strftime("%Y-%m-%d"), - "jids": { - "2": choice_str - }, + "jids": {"2": choice_str}, "view": "pc_listok", - "pravo": "Student" + "pravo": "Student", } data = { "akcia": "ulozJedlaStravnika", - "jedlaStravnika": json.dumps(boarder_menu) + "jedlaStravnika": json.dumps(boarder_menu), } response = edupage.session.post(request_url, data=data).content.decode() @@ -104,7 +102,9 @@ class Lunches(Module): @ModuleHelper.logged_in def get_lunch(self, date: datetime): date_strftime = date.strftime("%Y%m%d") - request_url = f"https://{self.edupage.subdomain}.edupage.org/menu/?date={date_strftime}" + request_url = ( + f"https://{self.edupage.subdomain}.edupage.org/menu/?date={date_strftime}" + ) response = self.edupage.session.get(request_url).content.decode() lunch_data = json.loads(response.split("edupageData: ")[1].split(",\r\n")[0]) @@ -171,13 +171,26 @@ def get_lunch(self, date: datetime): quantity_average = quantity.get("priemer") quantity_ratings = quantity.get("pocet") - rating = Rating(date.strftime("%Y-%m-%d"), boarder_id, - quality_average, quantity_average, - quality_ratings, quantity_ratings) + rating = Rating( + date.strftime("%Y-%m-%d"), + boarder_id, + quality_average, + quantity_average, + quality_ratings, + quantity_ratings, + ) else: rating = None menus.append(Menu(name, allergens, weight, number, rating)) - return Lunch(served_from, served_to, amount_of_foods, - chooseable_menus, can_be_changed_until, - title, menus, date, boarder_id) + return Lunch( + served_from, + served_to, + amount_of_foods, + chooseable_menus, + can_be_changed_until, + title, + menus, + date, + boarder_id, + ) diff --git a/edupage_api/messages.py b/edupage_api/messages.py index 512f308..c72cfaa 100644 --- a/edupage_api/messages.py +++ b/edupage_api/messages.py @@ -1,13 +1,16 @@ -from typing import Union import json +from typing import Union +from edupage_api.compression import RequestData from edupage_api.exceptions import InvalidRecipientsException, RequestError from edupage_api.module import Module from edupage_api.people import EduAccount -from edupage_api.compression import RequestData + class Messages(Module): - def send_message(self, recipients: Union[list[EduAccount], EduAccount, list[str]], body: str) -> int: + def send_message( + self, recipients: Union[list[EduAccount], EduAccount, list[str]], body: str + ) -> int: recipient_string = "" if isinstance(recipients, list): @@ -21,17 +24,17 @@ def send_message(self, recipients: Union[list[EduAccount], EduAccount, list[str] else: recipient_string = recipients.get_id() - data = RequestData.encode_request_body({ - "selectedUser": recipient_string, - "text": body, - "attachements": "{}", - "receipt": "0", - "typ": "sprava", - }) + data = RequestData.encode_request_body( + { + "selectedUser": recipient_string, + "text": body, + "attachements": "{}", + "receipt": "0", + "typ": "sprava", + } + ) - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } + headers = {"Content-Type": "application/x-www-form-urlencoded"} request_url = f"https://{self.edupage.subdomain}.edupage.org/timeline/?=&akcia=createItem&eqav=1&maxEqav=7" response = self.edupage.session.post(request_url, data=data, headers=headers) @@ -39,11 +42,13 @@ def send_message(self, recipients: Union[list[EduAccount], EduAccount, list[str] response_text = RequestData.decode_response(response.text) if response_text == "0": raise RequestError("Edupage returned an error response") - + response = json.loads(response_text) - + changes = response.get("changes") if changes == [] or changes is None: - raise RequestError("Failed to send message (edupage returned an empty 'changes' array) - https://github.com/ivanhrabcak/edupage-api/issues/62") - - return int(changes[0].get("timelineid")) \ No newline at end of file + raise RequestError( + "Failed to send message (edupage returned an empty 'changes' array) - https://github.com/ivanhrabcak/edupage-api/issues/62" + ) + + return int(changes[0].get("timelineid")) diff --git a/edupage_api/module.py b/edupage_api/module.py index 4e40676..949463a 100644 --- a/edupage_api/module.py +++ b/edupage_api/module.py @@ -38,6 +38,7 @@ def parse_int(val: str) -> Optional[int]: """ If any argument of this function is none, it throws MissingDataException """ + @staticmethod def assert_none(*args): if None in args: @@ -85,6 +86,7 @@ def strptime_or_none(date_string: str, format: str) -> Optional[datetime]: Throws NotLoggedInException if someone uses a method with this decorator and hasn't logged in yet """ + @staticmethod def logged_in(method): @wraps(method) @@ -93,6 +95,7 @@ def __impl(self: Module, *method_args, **method_kwargs): raise NotLoggedInException() return method(self, *method_args, **method_kwargs) + return __impl @staticmethod @@ -104,18 +107,19 @@ def __impl(self, *method_args, **method_kwargs): return method(self, *method_args, **method_kwargs) return __impl - + """ Throws NotParentException if someone uses a method with this decorator and is not using a parent account """ + @staticmethod def is_parent(method): @wraps(method) def __impl(self: Module, *method_args, **method_kwargs): if "Rodic" not in self.edupage.get_user_id(): raise NotParentException() - + return method(self, *method_args, **method_kwargs) - + return __impl diff --git a/edupage_api/parent.py b/edupage_api/parent.py index 4f5622c..c7c4533 100644 --- a/edupage_api/parent.py +++ b/edupage_api/parent.py @@ -1,32 +1,32 @@ from typing import Union + +from edupage_api.exceptions import InvalidChildException, UnknownServerError from edupage_api.module import Module, ModuleHelper from edupage_api.people import EduAccount -from edupage_api.exceptions import InvalidChildException, UnknownServerError + class Parent(Module): @ModuleHelper.logged_in @ModuleHelper.is_parent def switch_to_child(self, child: Union[EduAccount, int]): - params = { - "studentid": child.person_id if type(child) == EduAccount else child - } + params = {"studentid": child.person_id if type(child) == EduAccount else child} url = f"https://{self.edupage.subdomain}.edupage.org/login/switchchild" response = self.edupage.session.get(url, params=params) if response.text != "OK": - raise InvalidChildException(f"{response.text}: Invalid child selected! (not your child?)") + raise InvalidChildException( + f"{response.text}: Invalid child selected! (not your child?)" + ) @ModuleHelper.logged_in @ModuleHelper.is_parent def switch_to_parent(self): # variable name is from edupage's code :/ rid = f"edupage;{self.edupage.subdomain};{self.edupage.username}" - - params = { - "rid": rid - } - + + params = {"rid": rid} + url = f"https://{self.edupage.subdomain}.edupage.org/login/edupageChange" response = self.edupage.session.get(url, params=params) diff --git a/edupage_api/people.py b/edupage_api/people.py index eba5c87..3129870 100644 --- a/edupage_api/people.py +++ b/edupage_api/people.py @@ -44,23 +44,28 @@ def recognize_account_type(person_data: dict) -> EduAccountType: return EduAccountType.PARENT @staticmethod - def parse(person_data: dict, person_id: int, edupage: EdupageModule) -> Optional[EduAccount]: + def parse( + person_data: dict, person_id: int, edupage: EdupageModule + ) -> Optional[EduAccount]: account_type = EduAccount.recognize_account_type(person_data) if account_type == EduAccountType.STUDENT: class_id = ModuleHelper.parse_int(person_data.get("classid")) name = DbiHelper(edupage).fetch_student_name(person_id) gender = Gender.parse(person_data.get("gender")) - student_since = ModuleHelper.strptime_or_none(person_data.get("datefrom"), "%Y-%m-%d") + student_since = ModuleHelper.strptime_or_none( + person_data.get("datefrom"), "%Y-%m-%d" + ) number_in_class = ModuleHelper.parse_int(person_data.get("numberinclass")) ModuleHelper.assert_none(name) - return EduStudent(person_id, name, gender, student_since, class_id, number_in_class) + return EduStudent( + person_id, name, gender, student_since, class_id, number_in_class + ) elif account_type == EduAccountType.TEACHER: classroom_id = person_data.get("classroomid") - classroom_name = DbiHelper(edupage).fetch_classroom_number( - classroom_id) + classroom_name = DbiHelper(edupage).fetch_classroom_number(classroom_id) name = DbiHelper(edupage).fetch_teacher_name(person_id) @@ -75,7 +80,9 @@ def parse(person_data: dict, person_id: int, edupage: EdupageModule) -> Optional else: teacher_to = None - return EduTeacher(person_id, name, gender, teacher_since, classroom_name, teacher_to) + return EduTeacher( + person_id, name, gender, teacher_since, classroom_name, teacher_to + ) else: return None @@ -85,21 +92,30 @@ def get_id(self): @dataclass class EduStudent(EduAccount): - def __init__(self, person_id: int, name: str, gender: Gender, in_school_since: Optional[datetime], - class_id: int, number_in_class: int): - super().__init__(person_id, name, gender, in_school_since, EduAccountType.STUDENT) + def __init__( + self, + person_id: int, + name: str, + gender: Gender, + in_school_since: Optional[datetime], + class_id: int, + number_in_class: int, + ): + super().__init__( + person_id, name, gender, in_school_since, EduAccountType.STUDENT + ) self.class_id = class_id self.number_in_class = number_in_class self.__student_only = False - + def get_id(self): if not self.__student_only: return super().get_id() else: return super().get_id().replace("Student", "StudentOnly") - + def set_student_only(self, student_only: bool): self.__student_only = student_only @@ -113,15 +129,32 @@ class EduStudentSkeleton: @dataclass class EduParent(EduAccount): - def __init__(self, person_id: int, name: str, gender: Gender, in_school_since: Optional[datetime]): - super().__init__(person_id, name, gender, in_school_since, EduAccountType.PARENT) + def __init__( + self, + person_id: int, + name: str, + gender: Gender, + in_school_since: Optional[datetime], + ): + super().__init__( + person_id, name, gender, in_school_since, EduAccountType.PARENT + ) @dataclass class EduTeacher(EduAccount): - def __init__(self, person_id: int, name: str, gender: Gender, in_school_since: Optional[datetime], - classroom_name: str, teacher_to: Optional[datetime]): - super().__init__(person_id, name, gender, in_school_since, EduAccountType.TEACHER) + def __init__( + self, + person_id: int, + name: str, + gender: Gender, + in_school_since: Optional[datetime], + classroom_name: str, + teacher_to: Optional[datetime], + ): + super().__init__( + person_id, name, gender, in_school_since, EduAccountType.TEACHER + ) self.teacher_to = teacher_to self.classroom_name = classroom_name @@ -159,10 +192,10 @@ def get_all_students(self) -> Optional[list[EduStudent]]: "op": "fetch", "needed_part": { "students": ["id", "classid", "short"], - } - } + }, + }, ], - "__gsh": self.edupage.gsec_hash + "__gsh": self.edupage.gsec_hash, } response = self.edupage.session.post(request_url, json=data).content.decode() @@ -174,7 +207,9 @@ def get_all_students(self) -> Optional[list[EduStudent]]: student_class_id = int(student["classid"]) if student["classid"] else None student_name_short = student["short"] - student = EduStudentSkeleton(student_id, student_name_short, student_class_id) + student = EduStudentSkeleton( + student_id, student_name_short, student_class_id + ) result.append(student) return result diff --git a/edupage_api/substitution.py b/edupage_api/substitution.py index 8deb838..9489bb5 100644 --- a/edupage_api/substitution.py +++ b/edupage_api/substitution.py @@ -7,7 +7,8 @@ from enum import Enum from typing import Optional, Union -from edupage_api.exceptions import ExpiredSessionException, InvalidTeacherException +from edupage_api.exceptions import (ExpiredSessionException, + InvalidTeacherException) from edupage_api.module import Module, ModuleHelper from edupage_api.people import EduTeacher, People @@ -32,34 +33,32 @@ class TimetableChange: class Substitution(Module): def __get_substitution_data(self, date: date) -> str: - url = (f"https://{self.edupage.subdomain}.edupage.org/substitution/server/viewer.js" - "?__func=getSubstViewerDayDataHtml") + url = ( + f"https://{self.edupage.subdomain}.edupage.org/substitution/server/viewer.js" + "?__func=getSubstViewerDayDataHtml" + ) data = { - "__args": [ - None, - { - "date": date.strftime("%Y-%m-%d"), - "mode": "classes" - } - ], - "__gsh": self.edupage.gsec_hash + "__args": [None, {"date": date.strftime("%Y-%m-%d"), "mode": "classes"}], + "__gsh": self.edupage.gsec_hash, } response = self.edupage.session.post(url, json=data).content.decode() response = json.loads(response) if response.get("reload"): - raise ExpiredSessionException("Invalid gsec hash! " - "(Expired session, try logging in again!)") + raise ExpiredSessionException( + "Invalid gsec hash! " "(Expired session, try logging in again!)" + ) return response.get("r") @ModuleHelper.logged_in def get_missing_teachers(self, date: date) -> Optional[list[EduTeacher]]: html = self.__get_substitution_data(date) - missing_teachers_string = (html.split("")[1] - .split("")[0]) + missing_teachers_string = html.split('')[ + 1 + ].split("")[0] if not missing_teachers_string: return None @@ -68,11 +67,14 @@ def get_missing_teachers(self, date: date) -> Optional[list[EduTeacher]]: all_teachers = People(self.edupage).get_teachers() - missing_teachers = [item for sublist in [ - (t.strip() - .split(" (")[0]).split(" + ") - for t in missing_teachers.split(", ") - ] for item in sublist] + missing_teachers = [ + item + for sublist in [ + (t.strip().split(" (")[0]).split(" + ") + for t in missing_teachers.split(", ") + ] + for item in sublist + ] try: missing_teachers = [ @@ -80,8 +82,10 @@ def get_missing_teachers(self, date: date) -> Optional[list[EduTeacher]]: for t in missing_teachers ] except IndexError: - raise InvalidTeacherException("Invalid teacher in substitution! " - "(The teacher is no longer frequenting this school)") + raise InvalidTeacherException( + "Invalid teacher in substitution! " + "(The teacher is no longer frequenting this school)" + ) return missing_teachers @@ -89,35 +93,41 @@ def get_missing_teachers(self, date: date) -> Optional[list[EduTeacher]]: def get_timetable_changes(self, date: date) -> Optional[list[TimetableChange]]: html = self.__get_substitution_data(date) - class_delim = ("