from __future__ import annotations import datetime import json import logging import re from html import unescape from typing import ( Union, List, Callable, Any, TypeVar, Optional, Tuple, Set, Iterable, TYPE_CHECKING, ) from urllib.parse import quote from autoslot import Slots # type: ignore from Crypto.Util import Padding if TYPE_CHECKING: from .clients import ClientBase, Client from .exceptions import ( DataError, DiscussionClosed, ParsingError, DateParsingError, UnsupportedOperation, ) from itertools import chain __all__ = ( "Util", "Object", "Subject", "Absence", "Period", "Average", "Grade", "Attachment", "LessonContent", "Lesson", "Homework", "Information", "Recipient", "Message", "Discussion", "ClientInfo", "Acquisition", "Evaluation", "Identity", "Guardian", "Student", "StudentClass", "Menu", "Punishment", "Delay", "TeachingStaff", "Report", ) log = logging.getLogger(__name__) def _get_l(d: dict) -> str: return d["L"] T = TypeVar("T") def noop(any: T) -> T: return any class MissingType: pass class Util: """Utilities for the API wrapper""" grade_translate = [ "Absent", "Dispense", "NonNote", "Inapte", "NonRendu", "AbsentZero", "NonRenduZero", "Felicitations", ] @classmethod def get(cls, iterable: Iterable[T], **kwargs: Any) -> list[T]: """Gets items from the list with the attributes specified. Args: iterable (list): The iterable to loop over """ output = [] for i in iterable: for attr in kwargs: if not hasattr(i, attr) or getattr(i, attr) != kwargs[attr]: break else: output.append(i) return output @classmethod def grade_parse(cls, string: str) -> str: if "|" in string: return cls.grade_translate[int(string[1]) - 1] else: return string @staticmethod def date_parse(formatted_date: str) -> datetime.date: """convert date to a datetime.date object""" if re.match(r"\d{2}/\d{2}/\d{4}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%Y").date() elif re.match(r"\d{2}/\d{2}/\d{2}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%y").date() elif re.match(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$", formatted_date): return datetime.datetime.strptime( formatted_date, "%d/%m/%Y %H:%M:%S" ).date() elif re.match(r"\d{2}/\d{2}/\d{2} \d{2}h\d{2}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%y %Hh%M").date() elif re.match(r"\d{2}/\d{2}", formatted_date): formatted_date += f"/{datetime.date.today().year}" return datetime.datetime.strptime(formatted_date, "%d/%m/%Y").date() elif re.match(r"\d{2}\d{2}$", formatted_date): date = datetime.date.today() hours = int(formatted_date[:2]) minutes = int(formatted_date[2:]) formatted_date = f"{date:%d}/{date:%m}/{date.year} {hours}h{minutes}" return datetime.datetime.strptime(formatted_date, "%d/%m/%Y %Hh%M").date() else: raise DateParsingError("Could not parse date", formatted_date) @staticmethod def datetime_parse(formatted_date: str) -> datetime.datetime: """convert date to a datetime.datetime object""" if re.match(r"\d{2}/\d{2}/\d{4}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%Y") elif re.match(r"\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%Y %H:%M:%S") elif re.match(r"\d{2}/\d{2}/\d{2} \d{2}h\d{2}$", formatted_date): return datetime.datetime.strptime(formatted_date, "%d/%m/%y %Hh%M") else: raise DateParsingError("Could not parse date", formatted_date) @staticmethod def html_parse(html_text: str) -> str: """remove tags from html text""" return unescape(re.sub(re.compile("<.*?>"), "", html_text)) @staticmethod def place2time(listeHeures: List, place: int) -> datetime.time: if place > len(listeHeures): # might be wrong... works with demo place = place % (len(listeHeures) - 1) start_time = next( filter(lambda x: x["G"] == place, listeHeures), None, ) if start_time is None: raise ValueError(f"Could not find starting time for place {place}") start_time = datetime.datetime.strptime(start_time["L"], "%Hh%M").time() return start_time class Object(Slots): """ Base object for all pronotepy data classes. """ __slots__ = ("_resolver",) class _Resolver: """ Resolves an arbitrary value from a json dictionary. """ R = TypeVar("R") _missing: MissingType = MissingType() def __init__(self, json_dict: dict): self.json_dict = json_dict def __call__( self, converter: Callable[[Any], R], *path: str, default: Union[MissingType, R] = _missing, strict: bool = True, ) -> R: """ Resolves an arbitrary value from a json dictionary Args: converter (Callable[[Any], R]): the final value will be passed to this converter, it can be any callable with a single argument path (str): arguments describing the path through the dictionary to the value default (Union[MissingType, R]): default value if the actual one cannot be found, works with strict as False strict (bool): if False, the resolver will return None when it can't find the correct value Returns: the resolved value """ json_value: Any = self.json_dict try: for p in path: # walk through the json dict according to the path json_value = json_value[p] except KeyError as e: # we have failed to get the correct value, try to return a default if default is not self._missing: log.debug( f"Could not get value for (path: %s), setting to default.", ",".join(path), ) json_value = default elif strict: # in strict mode we do not want to give unpredictable output log.debug("Could not follow path in:") log.debug(json.dumps(self.json_dict)) log.debug(path) raise ParsingError( "Could not follow path", self.json_dict, path ) from e else: json_value = None else: try: json_value = converter(json_value) except Exception as e: log.debug( "Could not convert value %s(%s) with %s", type(json_value), json_value, converter, ) raise ParsingError( f"Error while converting value: {e}", self.json_dict, path ) from e return json_value def __init__(self, json_dict: dict) -> None: self._resolver: Object._Resolver = self._Resolver(json_dict) def to_dict( self, exclude: Set[str] = set(), include_properties: bool = False ) -> dict: """ Recursively serializes this object into a dict .. note:: Does not check for loops, which are currently handled on a case to case basis. Args: exclude (Set[str]): items to exclude from serialization include_properties (bool): whether to evaluate properties. This may be useful for example for a :class:`Period` for which you want to serialize its grades too. :attr:`Period.grades` is a property that is evaluated only when called. .. warning:: Setting this option to `True` can be **extremely** inefficient! Returns: dict: A dictionary containing all non-private properties """ def serialize_slot(slot: Any) -> Any: if isinstance(slot, Object): return slot.to_dict() else: # Assume all other values are primitives return slot serialized = {} # Join __slots__ with a list of all properties if include_properties is True # otherwise just iterate overs __slots__ to_iter = ( chain( self.__slots__, [ prop for prop in dir(self.__class__) if not prop.startswith("_") and isinstance(getattr(self.__class__, prop), property) ], ) if include_properties else self.__slots__ ) for slot_name in to_iter: # type: ignore if slot_name.startswith("_") or slot_name in exclude: # Skip private and excluded slots continue slot = getattr(self, slot_name) serialized[slot_name] = ( [serialize_slot(v) for v in slot] if isinstance(slot, list) else serialize_slot(slot) ) return serialized class Subject(Object): """ Represents a subject. You shouldn't have to create this class manually. Attributes: id (str): the id of the subject (used internally) name (str): name of the subject groups (bool): if the subject is in groups """ def __init__(self, parsed_json: dict) -> None: super().__init__(parsed_json) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.groups: bool = self._resolver(bool, "estServiceGroupe", default=False) class Report(Object): """Represents a student report. You shouldn't have to create this class manually. Attributes: subjects (List[ReportSubject]): the subjects that are present in the report comments (List[str]): the global report comments """ class ReportSubject(Object): """ Represents a subject found in a report. You shouldn't have to create this class manually. Attributes: id (str): the id of the subject (used internally) name (str): name of the subject color (str): the color of the subject comments (List[str]): the list of the subject's comments class_average (Optional[str]): the average of the class student_average (Optional[str]): the average of the student min_average (Optional[str]): the lowest average of the class max_average (Optional[str]): the highest average of the class coefficient (Optional[str]): the coefficient of the subject teachers (List[str]): the subject's teachers' names """ def __init__(self, parsed_json: dict) -> None: super().__init__(parsed_json) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.color: str = self._resolver(str, "couleur") self.comments: List[str] = self._resolver( lambda l: [c["L"] for c in l if "L" in c], "ListeAppreciations", "V" ) def grade_or_none(grade: Any) -> Optional[str]: return Util.grade_parse(grade) if grade else None self.class_average: Optional[str] = self._resolver( grade_or_none, "MoyenneClasse", "V", strict=False ) self.student_average: Optional[str] = self._resolver( grade_or_none, "MoyenneEleve", "V", strict=False ) self.min_average: Optional[str] = self._resolver( grade_or_none, "MoyenneInf", "V", strict=False ) self.max_average: Optional[str] = self._resolver( grade_or_none, "MoyenneSup", "V", strict=False ) self.coefficient: Optional[str] = self._resolver( str, "Coefficient", "V", strict=False ) self.teachers: List[str] = self._resolver( lambda l: [i["L"] for i in l], "ListeProfesseurs", "V", default=[] ) del self._resolver def __init__(self, parsed_json: dict) -> None: super().__init__(parsed_json) self.subjects: List[Report.ReportSubject] = self._resolver( lambda l: [Report.ReportSubject(s) for s in l], "ListeServices", "V", default=[], ) self.comments: List[str] = self._resolver( lambda l: [c["L"] for c in l if "L" in c], "ObjetListeAppreciations", "V", "ListeAppreciations", "V", default=[], ) class Absence(Object): """ Represents an absence with a given period. You shouldn't have to create this class manually. Attributes: id (str): the id of the absence (used internally) from_date (datetime.datetime): starting time of the absence to_date (datetime.datetime): end of the absence justified (bool): is the absence justified hours (str): the number of hours missed days (int): the number of days missed reasons (List[str]): The reason(s) for the absence """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.from_date: datetime.datetime = self._resolver( Util.datetime_parse, "dateDebut", "V" ) self.to_date: datetime.datetime = self._resolver( Util.datetime_parse, "dateFin", "V" ) self.justified: bool = self._resolver(bool, "justifie", default=False) self.hours: Optional[str] = self._resolver(str, "NbrHeures", strict=False) self.days: int = self._resolver(int, "NbrJours", default=0) self.reasons: List[str] = self._resolver( lambda l: [i["L"] for i in l], "listeMotifs", "V", default=[] ) del self._resolver class Delay(Object): """ Represents a delay with a given period. You shouldn't have to create this class manually. Attributes: id (str): the id of the delay (used internally) date (datetime.datetime): date of the delay minutes (str): the number of minutes missed justified (bool): is the delay justified justification (str): the justification for the delay reasons (List[str]): The reason(s) for the delay """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.date: datetime.datetime = self._resolver(Util.datetime_parse, "date", "V") self.minutes: int = self._resolver(int, "duree", default=0) self.justified: bool = self._resolver(bool, "justifie", default=False) self.justification: Optional[str] = self._resolver( str, "justification", strict=False ) self.reasons: List[str] = self._resolver( lambda l: [i["L"] for i in l], "listeMotifs", "V", default=[] ) del self._resolver class Period(Object): """ Represents a period of the school year. You shouldn't have to create this class manually. Attributes: id (str): the id of the period (used internally) name (str): name of the period start (datetime.datetime): date on which the period starts end (datetime.datetime): date on which the period ends """ instances: Set[Any] = set() def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.__class__.instances.add(self) self._client = client self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.start: datetime.datetime = self._resolver( Util.datetime_parse, "dateDebut", "V" ) self.end: datetime.datetime = self._resolver( Util.datetime_parse, "dateFin", "V" ) del self._resolver @property def report(self) -> Optional[Report]: """ Gets a report from a period. Returns: Optional[Report]: When ``None``, then the report is not yet published or is unavailable for any other reason """ json_data = {"periode": {"G": 2, "N": self.id, "L": self.name}} data = self._client.post("PageBulletins", 13, json_data)["dataSec"]["data"] return Report(data) if "Message" not in data else None @property def grades(self) -> List["Grade"]: """Get grades from the period.""" json_data = {"Periode": {"N": self.id, "L": self.name}} response = self._client.post("DernieresNotes", 198, json_data) grades = response["dataSec"]["data"]["listeDevoirs"]["V"] return [Grade(g) for g in grades] @property def averages(self) -> List["Average"]: """Get averages from the period.""" json_data = {"Periode": {"N": self.id, "L": self.name}} response = self._client.post("DernieresNotes", 198, json_data) crs = response["dataSec"]["data"]["listeServices"]["V"] try: return [Average(c) for c in crs] except ParsingError as e: if e.path == ["moyEleve", "V"]: raise UnsupportedOperation("Could not get averages") raise @property def overall_average(self) -> str: """Get overall average from the period. If the period average is not provided by pronote, then it's calculated. Calculation may not be the same as the actual average. (max difference 0.01)""" json_data = {"Periode": {"N": self.id, "L": self.name}} response = self._client.post("DernieresNotes", 198, json_data) average = response["dataSec"]["data"].get("moyGenerale") if average: return average["V"] # VVVVVVVV will be removed in v3.0.0 elif response["dataSec"]["data"]["listeServices"]["V"]: a: float = 0 total = 0 services = response["dataSec"]["data"]["listeServices"]["V"] for s in services: try: avrg = s["moyEleve"]["V"].replace(",", ".") except KeyError: raise UnsupportedOperation("Could not get averages") try: flt = float(avrg) except ValueError: flt = False if flt: a += flt total += 1 average = round(a / total, 2) if total else -1 else: average = -1 return str(average) @property def class_overall_average(self) -> Optional[str]: """Get group average from the period.""" json_data = {"Periode": {"N": self.id, "L": self.name}} response = self._client.post("DernieresNotes", 198, json_data) average = response["dataSec"]["data"].get("moyGeneraleClasse") if average: return average["V"] else: return None @property def evaluations(self) -> List["Evaluation"]: """ All evaluations from this period """ json_data = {"periode": {"N": self.id, "L": self.name, "G": 2}} response = self._client.post("DernieresEvaluations", 201, json_data) evaluations = response["dataSec"]["data"]["listeEvaluations"]["V"] return [Evaluation(e) for e in evaluations] @property def absences(self) -> List[Absence]: """ All absences from this period """ json_data = { "periode": {"N": self.id, "L": self.name, "G": 2}, "DateDebut": {"_T": 7, "V": self.start.strftime("%d/%m/%Y %H:%M:%S")}, "DateFin": {"_T": 7, "V": self.end.strftime("%d/%m/%Y %H:%M:%S")}, } response = self._client.post("PagePresence", 19, json_data) absences = response["dataSec"]["data"]["listeAbsences"]["V"] return [Absence(a) for a in absences if a["G"] == 13] @property def delays(self) -> List[Delay]: """ All delays from this period """ json_data = { "periode": {"N": self.id, "L": self.name, "G": 2}, "DateDebut": {"_T": 7, "V": self.start.strftime("%d/%m/%Y %H:%M:%S")}, "DateFin": {"_T": 7, "V": self.end.strftime("%d/%m/%Y %H:%M:%S")}, } response = self._client.post("PagePresence", 19, json_data) delays = response["dataSec"]["data"]["listeAbsences"]["V"] return [Delay(a) for a in delays if a["G"] == 14] @property def punishments(self) -> List[Punishment]: """ All punishments from a given period """ json_data = { "periode": {"N": self.id, "L": self.name, "G": 2}, "DateDebut": {"_T": 7, "V": self.start.strftime("%d/%m/%Y %H:%M:%S")}, "DateFin": {"_T": 7, "V": self.end.strftime("%d/%m/%Y %H:%M:%S")}, } response = self._client.post("PagePresence", 19, json_data) absences = response["dataSec"]["data"]["listeAbsences"]["V"] return [Punishment(self._client, a) for a in absences if a["G"] == 41] class Average(Object): """ Represents an Average. Attributes: student (str): students average in the subject class_average (str): classes average in the subject max (str): highest average in the class min (str): lowest average in the class out_of (str): maximum amount of points default_out_of (str): the default maximum amount of points subject (Subject): subject the average is from background_color (str): background color of the subject """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.student: str = self._resolver(Util.grade_parse, "moyEleve", "V") self.out_of: str = self._resolver(Util.grade_parse, "baremeMoyEleve", "V") self.default_out_of: str = self._resolver( Util.grade_parse, "baremeMoyEleveParDefault", "V", default="" ) self.class_average: str = self._resolver(Util.grade_parse, "moyClasse", "V") self.min: str = self._resolver(Util.grade_parse, "moyMin", "V") self.max: str = self._resolver(Util.grade_parse, "moyMax", "V") self.subject = Subject(json_dict) self.background_color: Optional[str] = self._resolver( str, "couleur", strict=False ) del self._resolver class Grade(Object): """Represents a grade. You shouldn't have to create this class manually. Attributes: id (str): the id of the grade (used internally) grade (str): the actual grade out_of (str): the maximum amount of points default_out_of (Optional[str]): the default maximum amount of points date (datetime.date): the date on which the grade was given subject (Subject): the subject in which the grade was given period (Period): the period in which the grade was given average (Optional[str]): the average of the class max (Optional[str]): the highest grade of the test min (Optional[str]): the lowest grade of the test coefficient (Optional[str]): the coefficient of the grade comment (Optional[str]): the comment on the grade description is_bonus (bool): is the grade bonus : only points above 10 count is_optionnal (bool): is the grade optionnal : the grade only counts if it increases the average is_out_of_20 (bool): is the grade out of 20. Example 8/10 -> 16/20 """ # TODO: optionnal -> optional def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.grade: str = self._resolver(Util.grade_parse, "note", "V") self.out_of: str = self._resolver(Util.grade_parse, "bareme", "V") self.default_out_of: str = self._resolver( Util.grade_parse, "baremeParDefaut", "V", strict=False ) self.date: datetime.date = self._resolver(Util.date_parse, "date", "V") self.subject: Subject = self._resolver(Subject, "service", "V") # TODO: remove, because it creates a loop when trying to `to_dict` self.period: Period = self._resolver( lambda p: Util.get(Period.instances, id=p)[0], "periode", "V", "N" ) self.average: str = self._resolver( Util.grade_parse, "moyenne", "V", strict=False ) self.max: str = self._resolver(Util.grade_parse, "noteMax", "V", strict=False) self.min: str = self._resolver(Util.grade_parse, "noteMin", "V", strict=False) self.coefficient: str = self._resolver(str, "coefficient", strict=False) self.comment: str = self._resolver(str, "commentaire", strict=False) self.is_bonus: bool = self._resolver(bool, "estBonus", default=False) self.is_optionnal: bool = ( self._resolver(bool, "estFacultatif", default=False) and not self.is_bonus ) self.is_out_of_20: bool = self._resolver(bool, "estRamenerSur20", default=False) del self._resolver def to_dict( self, exclude: Set[str] = set(), include_properties: bool = False ) -> dict: # Exclude self.period, because it would otherwise cause a loop return super().to_dict( exclude={ "period", }.union(exclude), include_properties=include_properties, ) class Attachment(Object): """ Represents a attachment to homework for example Attributes: name (str): Name of the file or url of the link. id (str): id of the file (used internally and for url) url (str): url of the file/link type (int): type of the attachment (0 = link, 1 = file) """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self.name: str = self._resolver(str, "L", default="") self.id: str = self._resolver(str, "N") self.type: int = self._resolver(int, "G") # 0 link, 1 file if self.type == 0: url = self._resolver(str, "url", default=None) self.url: str = self.name if url is None else url else: padd = Padding.pad( json.dumps({"N": self.id, "Actif": True}).replace(" ", "").encode(), 16 ) magic_stuff = client.communication.encryption.aes_encrypt(padd).hex() self.url = ( f"{client.communication.root_site}/FichiersExternes/{magic_stuff}/" + quote(self.name, safe="~()*!.'") + f"?Session={client.attributes['h']}" ) self._data = None del self._resolver def save(self, file_name: Optional[str] = None) -> None: """ Saves the file on to local storage. Args: file_name (str): file name """ if self.type == 1: response = self._client.communication.session.get(self.url) if not file_name: file_name = self.name if response.status_code != 200: raise FileNotFoundError( "The file was not found on pronote. The url may be badly formed." ) with open(file_name, "wb") as handle: for block in response.iter_content(1024): handle.write(block) @property def data(self) -> bytes: """Gets the raw file data.""" if self._data: return self._data response = self._client.communication.session.get(self.url) return response.content class LessonContent(Object): """ Represents the content of a lesson. You shouldn't have to create this class manually. Attributes: title (Optional[str]): title of the lesson content description (Optional[str]): description of the lesson content category (Optional[str]): category of the lesson content """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self.title: Optional[str] = self._resolver(str, "L", strict=False) self.description: Optional[str] = self._resolver( Util.html_parse, "descriptif", "V", strict=False ) self.category: Optional[str] = self._resolver( str, "categorie", "V", "L", strict=False ) self._files: Tuple[Any, ...] = self._resolver(tuple, "ListePieceJointe", "V") del self._resolver @property def files(self) -> List[Attachment]: """Get all the attached files from the lesson""" return [Attachment(self._client, jsn) for jsn in self._files] class Lesson(Object): """ Represents a lesson with a given time. You shouldn't have to create this class manually. Attributes: id (str): the id of the lesson (used internally) subject (Optional[Subject]): the subject that the lesson is from teacher_name (Optional[str]): name of the teacher teacher_names (Optional[List[str]]): name of the teachers classroom (Optional[str]): name of the classroom classrooms (Optional[List[str]]): name of the classrooms canceled (bool): if the lesson is canceled status (Optional[str]): status of the lesson background_color (Optional[str]): background color of the lesson outing (bool): if it is a pedagogical outing start (datetime.datetime): starting time of the lesson end (datetime.datetime): end of the lesson memo (Optional[str]): memo of the lesson group_name (Optional[str]): Name of the group. group_names (Optional[List[str]]): Name of the groups. exempted (bool): Specifies if the student's presence is exempt. virtual_classrooms (List[str]): List of urls for virtual classrooms num (int): For the same lesson time, the biggest num is the one shown on pronote. detention (bool): is marked as detention test (bool): if there will be a test in the lesson """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self._content: Optional[LessonContent] = None self.id: str = self._resolver(str, "N") self.canceled: bool = self._resolver(bool, "estAnnule", default=False) self.status: Optional[str] = self._resolver(str, "Statut", strict=False) self.memo: Optional[str] = self._resolver(str, "memo", strict=False) self.background_color: Optional[str] = self._resolver( str, "CouleurFond", strict=False ) self.outing: bool = self._resolver(bool, "estSortiePedagogique", default=False) self.start: datetime.datetime = self._resolver( Util.datetime_parse, "DateDuCours", "V" ) self.exempted: bool = self._resolver(bool, "dispenseEleve", default=False) self.virtual_classrooms: List[str] = self._resolver( lambda l: [i["url"] for i in l], "listeVisios", "V", default=[] ) self.num: int = self._resolver(int, "P", default=0) self.detention: bool = self._resolver(bool, "estRetenue", default=False) self.test: bool = self._resolver( bool, "cahierDeTextes", "V", "estDevoir", default=False ) self.end: datetime.datetime = self._resolver( Util.datetime_parse, "DateDuCoursFin", "V", strict=False ) if self.end is None: end_times = client.func_options["dataSec"]["data"]["General"][ "ListeHeuresFin" ]["V"] # get correct ending time # Pronote gives us the place where the hour should be in a week, when # we modulo that with the amount of hours in a day we can get the "place" when the # hour starts. Then we just add the duration (and substract 1) end_place = ( json_dict["place"] % (len(end_times) - 1) + json_dict["duree"] - 1 ) # With the end "place" now known we can look up the ending time in func_options end_time = Util.place2time(end_times, end_place) self.end = self.start.replace(hour=end_time.hour, minute=end_time.minute) # get additional information about the lesson self.teacher_names: Optional[List[str]] = [] self.classrooms: Optional[List[str]] = [] self.group_names: Optional[List[str]] = [] self.subject: Optional[Subject] = None if "ListeContenus" not in json_dict: raise ParsingError( "Error while parsing for lesson details", json_dict, ("ListeContenus", "V"), ) for d in json_dict["ListeContenus"]["V"]: if "G" not in d: continue elif d["G"] == 16: self.subject = Subject(d) elif d["G"] == 3: self.teacher_names.append(d["L"]) elif d["G"] == 17: self.classrooms.append(d["L"]) elif d["G"] == 2: self.group_names.append(d["L"]) # All values joined together to prevent breaking changes self.teacher_name: Optional[str] = ( ", ".join(self.teacher_names) if self.teacher_names else None ) self.classroom: Optional[str] = ( ", ".join(self.classrooms) if self.classrooms else None ) self.group_name: Optional[str] = ( ", ".join(self.group_names) if self.group_names else None ) del self._resolver @property def normal(self) -> bool: """Is the lesson considered normal (is not detention, or an outing).""" if self.detention is None and self.outing is None: return True return False @property def content(self) -> Optional[LessonContent]: """ Gets content of the lesson. May be None if there is no description. .. note:: This property is very inefficient and will send a request to pronote, so don't use it often. """ if self._content: return self._content week = self._client.get_week(self.start.date()) data = {"domaine": {"_T": 8, "V": f"[{week}..{week}]"}} response = self._client.post("PageCahierDeTexte", 89, data) contents = {} for lesson in response["dataSec"]["data"]["ListeCahierDeTextes"]["V"]: if lesson["cours"]["V"]["N"] == self.id and lesson["listeContenus"]["V"]: contents = lesson["listeContenus"]["V"][0] break if not contents: return None self._content = LessonContent(self._client, contents) return self._content class Homework(Object): """ Represents a homework. You shouldn't have to create this class manually. Attributes: id (str): the id of the homework (used internally) subject (Subject): the subject that the homework is for description (str): the description of the homework background_color (str): the background color of the homework done (bool): if the homework is marked done date (datetime.date): deadline """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self.id: str = self._resolver(str, "N") self.description: str = self._resolver(Util.html_parse, "descriptif", "V") self.done: bool = self._resolver(bool, "TAFFait") self.subject: Subject = self._resolver(Subject, "Matiere", "V") self.date: datetime.date = self._resolver(Util.date_parse, "PourLe", "V") self.background_color: str = self._resolver(str, "CouleurFond") self._files = self._resolver(tuple, "ListePieceJointe", "V") del self._resolver def set_done(self, status: bool) -> None: """ Sets the status of the homework. Args: status (bool): The status to which to change """ data = {"listeTAF": [{"N": self.id, "TAFFait": status}]} self._client.post("SaisieTAFFaitEleve", 88, data) self.done = status @property def files(self) -> List[Attachment]: """Get all the files and links attached to the homework""" return [Attachment(self._client, jsn) for jsn in self._files] # type: ignore class Information(Object): """ Represents a information in a information and surveys tab. Attributes: id (str): the id of the information author (str): author of the information title (str): title of the information read (bool): if the message has been read creation_date (datetime.datetime): the date when the message was created start_date (datetime.datetime): the date when the message became visible end_date (datetime.datetime): the date on which the message will be withdrawn category (str): category of the information survey (bool): if the message is a survey anonymous_response (bool): if the survey response is anonymous attachments (List[Attachment]) template (bool): if it is a template message shared_template (bool): if it is a shared template message """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self.id: str = self._resolver(str, "N") self.title: Optional[str] = self._resolver(str, "L", strict=False) self.author: str = self._resolver(str, "auteur") self._raw_content: list = self._resolver(list, "listeQuestions", "V") self.read: bool = self._resolver(bool, "lue") self.creation_date: datetime.datetime = self._resolver( Util.datetime_parse, "dateCreation", "V" ) self.start_date: Optional[datetime.datetime] = self._resolver( Util.datetime_parse, "dateDebut", "V", strict=False ) self.end_date: Optional[datetime.datetime] = self._resolver( Util.datetime_parse, "dateFin", "V", strict=False ) self.category: str = self._resolver(str, "categorie", "V", "L") self.survey: bool = self._resolver(bool, "estSondage") self.template: bool = self._resolver(bool, "estModele", default=False) self.shared_template: bool = self._resolver( bool, "estModelePartage", default=False ) self.anonymous_response: bool = self._resolver(bool, "reponseAnonyme") def make_attachments(questions: dict) -> List[Attachment]: attachments = [] for question in questions: for j in question["listePiecesJointes"]["V"]: attachments.append(Attachment(client, j)) return attachments self.attachments: List[Attachment] = self._resolver( make_attachments, "listeQuestions", "V" ) del self._resolver @property def content(self) -> str: """Content of the information""" return Util.html_parse(self._raw_content[0]["texte"]["V"]) def mark_as_read(self, status: bool) -> None: """Mark this information as read""" data = { "listeActualites": [ { "N": self.id, "validationDirecte": True, "genrePublic": 4, "public": { "N": self._client.info.id, "G": 4, }, "lue": status, } ], "saisieActualite": False, } self._client.post("SaisieActualites", 8, data) self.read = status class Recipient(Object): """ Represents a recipient to create a discussion Attributes: id (str): the id of the recipient (used internally) name (str): name of the recipient type (str): teacher or staff email (Optional[str]): email of the recipient functions (List[str]): all function or subject of the recipient with_discussion (bool): can be contacted by message """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self._type: int = self._resolver(int, "G") self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.type: str = "teacher" if self._type == 3 else "staff" self.email: Optional[str] = self._resolver(str, "email", strict=False) self.functions: List[str] = [] if self.type == "teacher": self.functions = self._resolver( lambda x: [r.get("L") for r in x], "listeRessources", "V" ) else: self.functions = self._resolver( lambda f: [f], "fonction", "V", "L", default=[] ) self.with_discussion: bool = self._resolver( bool, "avecDiscussion", default=False ) del self._resolver class Message(Object): """ Represents a message in a discussion. Attributes: id (str): the id of the message (used internally) author (Optional[str]): author of the message, if ``None``, then we are the author seen (bool): if the message was seen created (datetime.datetime): when the message was created content (str): content of the messages replying_to (Optional[Message]): the message that this is replying to, if None, it is the first message in a discussion """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self._client = client self._possession = self._resolver(str, "possessionMessage", "V", "N") self.id: str = self._resolver(str, "N") self.content: str = "" self.author: Optional[str] = ( None if self._resolver(bool, "emetteur", default=False) else self._resolver(str, "public_gauche") ) self.seen: bool = self._resolver(bool, "lu", default=False) self.created: datetime.datetime = self._resolver( Util.datetime_parse, "date", "V" ) # TODO: DEPRECATED self.date = self.created self.replying_to: Optional[Message] = None if json_dict.get("estHTML", False): self.content = self._resolver(Util.html_parse, "contenu", "V") else: self.content = self._resolver(str, "contenu") del self._resolver def recipients(self) -> List[str]: """ Recipients of this message """ resp = self._client.post( "SaisiePublicMessage", 131, {"message": {"N": self.id}} ) return [r["L"] for r in resp["dataSec"]["data"]["listeDest"]["V"]] def reply(self, message: str) -> None: """ Reply to this message Args: message (str) Raises: DiscussionClosed: when the parent discussion is closed and we cannot reply """ resp = self._client.post( "ListeMessages", 131, { "listePossessionsMessages": [{"N": self._possession}], "message": {"N": self.id}, }, ) if len(resp["dataSec"]["data"].get("listeBoutons", {"V": []})["V"]) == 0: raise DiscussionClosed("Cannot reply to discussion") msg = resp["dataSec"]["data"]["messagePourReponse"]["V"] button = resp["dataSec"]["data"]["listeBoutons"]["V"][0] self._client.post( "SaisieMessage", 131, { "messagePourReponse": msg, "contenu": message, "listeFichiers": [], "bouton": button, # pronote wants the specific button we pressed }, ) class Discussion(Object): """ Represents a discussion. A PRONOTE discussion is a channel that, in the web UI, has threads. The internal structure, however, looks more linear, with messages replying to other messages. You can get messages sent in a discussion using :attr:`.messages`. The messages will be in ascending chronological order (ending with the newest). Each message also has a :attr:`Message.replying_to` attribute specifying what message it is replying to. In most cases this will be just the previous message. For example, to get a chat like printout of a discussion: .. code-block:: python discussion: Discussion = ... print(discussion.subject or "(no subject)") print("-------------------") last_msg = None for message in discussion.messages: if message.replying_to != last_msg and message.replying_to: print(f" (replying to {message.replying_to.author or 'Me'}: {message.replying_to.content[:20]})") print(message.author or "Me", ">", message.content) print() last_msg = message Attributes: id (str): the id of the discussion (used internally) subject (str): the subject of the discussion, can be an empty string creator (Optional[str]): person that created the discussion, if ``None``, then we are the creator unread (int): number of unread messages closed (bool): if the discussion is closed labels (List[str]): labels on the discussion, usually can be ``["Trash"]``, or ``["Drafts"]`` replyable (bool): because pronotepy does not currently support replying to discussions that are inside other discussions, this boolean signifies if pronotepy is able to reply .. deprecated:: 2.10 Always ``True`` close (bool): if the discussion is closed .. deprecated:: 2.10 Use :attr:`.closed` instead """ def __init__(self, client: Client, json_dict: dict, labels: dict) -> None: super().__init__(json_dict) self._client = client self._possessions: list = self._resolver(noop, "listePossessionsMessages", "V") self._date_cache: Optional[datetime.datetime] = None self.replyable: bool = True self.subject: str = self._resolver(str, "objet") self.creator: Optional[str] = self._resolver(str, "initiateur", strict=False) self._participants_message_id = self._resolver( str, "messagePourParticipants", "V", "N" ) self.unread: int = self._resolver(int, "nbNonLus", default=0) self.closed: bool = self._resolver(bool, "ferme", default=False) self.close: bool = self.closed labels_str = {4: "Drafts", 5: "Trash"} self.labels: List[str] = self._resolver( lambda l: [labels_str[labels[i["N"]]] for i in l], "listeEtiquettes", "V", default=[], ) del self._resolver def participants(self) -> List[str]: """ Get participants in the discussion. A participant does not have to send a message to the discussion to be included. The names may be in the format ``NAME - KID'S NAME ...``. Ex: ``M. PARENT F. - PARENT Fanny (3A) PARENT Manon (6D)`` """ resp = self._client.post( "SaisiePublicMessage", 131, { "estDestinatairesReponse": False, "estPublicParticipant": True, "message": {"N": self._participants_message_id}, }, ) # the names are often in the format "NAME - KID'S NAME" return [i["L"] for i in resp["dataSec"]["data"]["listeDest"]["V"]] @property def messages(self) -> List[Message]: """ Messages linked to the discussion .. warning:: Makes a request everytime it is accessed .. TODO: should be a method instead """ resp = self._client.post( "ListeMessages", 131, {"listePossessionsMessages": self._possessions}, ) messages = {} for message_json in resp["dataSec"]["data"]["listeMessages"]["V"]: msg = Message(self._client, message_json) messages[msg.id] = msg for message_json in resp["dataSec"]["data"]["listeMessages"]["V"]: messages[message_json["N"]].replying_to = messages.get( message_json["messageSource"]["V"]["N"] ) return list(sorted(messages.values(), key=lambda x: x.created)) @property def date(self) -> datetime.datetime: """ Date when the discussion was opened. Alias for ``Discussion.messages[0].date``. """ if not self._date_cache: msgs = self.messages self._date_cache = msgs[0].date return self._date_cache def mark_as(self, read: bool) -> None: """ Mark as read/unread the discussion Args: read (bool): read/unread """ self._client.post( "SaisieMessage", 131, { "commande": "pourLu", "lu": read, "listePossessionsMessages": self._possessions, }, ) def reply(self, message: str) -> None: """ Reply to a discussion, this usually means replying to the last message Args: message (str) """ if self.closed: raise DiscussionClosed("Cannot reply to discussion") # get the message we should respond to resp = self._client.post( "ListeMessages", 131, {"listePossessionsMessages": self._possessions}, ) msg = resp["dataSec"]["data"]["messagePourReponse"]["V"] button = resp["dataSec"]["data"]["listeBoutons"]["V"][0] self._client.post( "SaisieMessage", 131, { "messagePourReponse": msg, "contenu": message, "listeFichiers": [], "bouton": button, # pronote wants the specific button we pressed }, ) def delete(self) -> None: """ Delete the discussion """ self._client.post( "SaisieMessage", 131, { "commande": "corbeille", "listePossessionsMessages": self._possessions, }, ) class ClientInfo(Slots): """ Contains info for a resource (a client). Attributes: id (str): id of the client (used internally) raw_resource (dict): Raw json defining the resource """ def __init__(self, client: ClientBase, json_: dict) -> None: self.id: str = json_["N"] self.raw_resource: dict = json_ self._client = client self.__cache: Optional[dict] = None @property def name(self) -> str: """ Name of the client """ return self.raw_resource["L"] @property def profile_picture(self) -> Optional[Attachment]: """ Profile picture of the client """ if self.raw_resource.get("avecPhoto"): return Attachment( self._client, {"L": "photo.jpg", "N": self.raw_resource["N"], "G": 1} ) else: return None @property def delegue(self) -> List[str]: """ list of classes of which the user is a delegue of """ if self.raw_resource.get("estDelegue"): return [ class_["L"] for class_ in self.raw_resource["listeClassesDelegue"]["V"] ] else: return [] @property def class_name(self) -> str: """ name of the student's class """ return self.raw_resource.get("classeDEleve", {}).get("L", "") @property def establishment(self) -> str: """ name of the student's establishment """ return self.raw_resource.get("Etablissement", {"V": {"L": ""}})["V"]["L"] def _cache(self) -> dict: if self.__cache is None: # this does not have all the protection _ClientBase.post provides, # but we need to manually add the resource id self.__cache = self._client.communication.post( "PageInfosPerso", {"Signature": {"onglet": 49, "ressource": {"N": self.id, "G": 4}}}, )["dataSec"]["data"]["Informations"] return self.__cache @property def address(self) -> tuple[str, str, str, str, str, str, str, str]: """ Address of the client Returns: A tuple of 8 elements: - 4 lines of address info - postal code - city - province - country """ c = self._cache() return ( c["adresse1"], c["adresse2"], c["adresse3"], c["adresse4"], c["codePostal"], c["ville"], c["province"], c["pays"], ) @property def email(self) -> str: """ Email of the client """ return self._cache()["eMail"] @property def phone(self) -> str: """ Phone of the client Returns: str: Phone in the format +[country-code][phone-number] """ c = self._cache() return "+" + c["indicatifTel"] + c["telephonePortable"] @property def ine_number(self) -> str: """ INE number of the client """ return self._cache()["numeroINE"] class Acquisition(Object): """ Contains acquisition info for an evaluation. Attributes: order (int): Telling the order in which the acquisition is. The list of acquisitions is already sorted by this. level (str): the level achieved for this acquisition id (int): id, used internally abbreviation (str): abbreviation for the level achieved coefficient (int): coefficient domain (str): domain in which the acquisition is domain_id (str) name (str): name (description) of the acquisition name_id (str) pillar (str) pillar_id (str) pillar_prefix (str) """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.level: str = self._resolver(str, "L") self.abbreviation: str = self._resolver(str, "abbreviation") self.coefficient: int = self._resolver(int, "coefficient") self.domain: str = self._resolver(str, "domaine", "V", "L") self.domain_id: str = self._resolver(str, "domaine", "V", "N") self.name: Optional[str] = self._resolver(str, "item", "V", "L", strict=False) self.name_id: Optional[str] = self._resolver( str, "item", "V", "N", strict=False ) self.order: int = self._resolver(int, "ordre") self.pillar: str = self._resolver(str, "pilier", "V", "L") self.pillar_id: str = self._resolver(str, "pilier", "V", "N") self.pillar_prefix: str = self._resolver(str, "pilier", "V", "strPrefixes") del self._resolver class Evaluation(Object): """ Data class for an evaluation. Attributes: name (str) id (str) domain (Optional[str]) teacher (str): the teacher who issued the evaluation coefficient (int) description (str) subject (Subject) paliers (List[str]) acquisitions (List[Acquisition]) date (datetime.date) """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.name: str = self._resolver(str, "L") self.id: str = self._resolver(str, "N") self.domain: Optional[str] = self._resolver( str, "domaine", "V", "L", strict=False ) self.teacher: str = self._resolver(str, "individu", "V", "L") self.coefficient: int = self._resolver(int, "coefficient") self.description: str = self._resolver(str, "descriptif") self.subject: Subject = self._resolver(Subject, "matiere", "V") self.paliers: List[str] = self._resolver( lambda x: [_get_l(y) for y in x], "listePaliers", "V" ) self.acquisitions: List[Acquisition] = self._resolver( lambda x: sorted([Acquisition(y) for y in x], key=lambda z: z.order), "listeNiveauxDAcquisitions", "V", ) self.date: datetime.date = self._resolver(Util.date_parse, "date", "V") del self._resolver class Identity(Object): """ Represents an Identity of a person Attributes: postal_code (str) date_of_birth (datetime.date) email (Optional[str]) last_name (str) country (str) mobile_number (Optional[str]) landline_number (Optional[str]) other_phone_number (Optional[str]) city (str) place_of_birth (Optional[str]) first_names (List[str]) address (List[str]) formatted_address (str): concatenated address information into a single string """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.postal_code: str = self._resolver(str, "CP") self.date_of_birth: Optional[datetime.date] = self._resolver( Util.date_parse, "dateNaiss", strict=False ) self.email: Optional[str] = self._resolver(str, "email", strict=False) self.last_name: str = self._resolver(str, "nom") self.country: str = self._resolver(str, "pays") self.mobile_number: Optional[str] = self._resolver(str, "telPort", strict=False) self.landline_number: Optional[str] = self._resolver( str, "telFixe", strict=False ) self.other_phone_number: Optional[str] = self._resolver( str, "telAutre", strict=False ) self.city: str = self._resolver(str, "ville") self.place_of_birth: Optional[str] = self._resolver( str, "villeNaiss", strict=False ) self.address: List[str] = [] i = 1 while True: option = json_dict.get("adresse" + str(i)) if not option: break self.address.append(option) i += 1 self.formatted_address: str = ",".join( [*self.address, self.postal_code, self.city, self.country] ) self.first_names: List[str] = [ json_dict.get("prenom", ""), json_dict.get("prenom2", ""), json_dict.get("prenom3", ""), ] del self._resolver class Guardian(Object): """ Represents a guardian of a student. Attributes: identity (Identity) accepteInfosProf (bool) authorized_email (bool) authorized_pick_up_kid (bool) urgency_contact (bool) preferred_responsible_contact (bool) accomodates_kid (bool) relatives_link (str) responsibility_level (str) financially_responsible (bool) full_name (str) is_legal (bool) """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.authorized_email: bool = self._resolver(bool, "autoriseEmail") self.authorized_pick_up_kid: bool = self._resolver( bool, "autoriseRecupererEnfant", default=False ) self.urgency_contact: bool = self._resolver( bool, "contactUrgence", default=False ) self.relatives_link: str = self._resolver(str, "lienParente") self.responsibility_level: str = self._resolver(str, "niveauResponsabilite") self.full_name: str = self._resolver(str, "nom") self.identity = Identity(json_dict) self.is_legal = self.responsibility_level == "LEGAL" del self._resolver class Student(Object): """ Represents a student Attributes: full_name (str) id (str) enrollment_date (datetime.date) date_of_birth (datetime.date) projects (List[str]) last_name (str) first_names (str) sex (str) options (List[str]): language options """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.full_name: str = self._resolver(str, "L") self.id: str = self._resolver(str, "N") self.enrollment_date: datetime.date = self._resolver( Util.date_parse, "entree", "V" ) self.date_of_birth: datetime.date = self._resolver(Util.date_parse, "neLe", "V") self.projects: List[str] = self._resolver( lambda p: [ f"{x.get('typeAmenagement', '')} ({x.get('handicap', '')})" for x in p ], "listeProjets", "V", ) self.last_name: str = self._resolver(str, "nom") self.first_names: str = self._resolver(str, "prenoms") self.sex: str = self._resolver(str, "sexe") self._client = client self._cache: Optional[dict] = None self.options = [] i = 1 while True: option = json_dict.get("option" + str(i)) if not option: break self.options.append(option) i += 1 del self._resolver @property def identity(self) -> Identity: """ Identity of this student """ if self._cache is None: self._cache = self._client.post( "FicheEleve", 105, {"Eleve": {"N": self.id}, "AvecEleve": True, "AvecResponsables": True}, ) return Identity(self._cache["dataSec"]["data"]["Identite"]["V"]) @property def guardians(self) -> List[Guardian]: """ List of responsible persons (parents). """ if self._cache is None: self._cache = self._client.post( "FicheEleve", 105, {"Eleve": {"N": self.id}, "AvecEleve": True, "AvecResponsables": True}, ) return [ Guardian(j) for j in self._cache["dataSec"]["data"]["Responsables"]["V"] ] class StudentClass(Object): """ Represents a class of students Attributes: name (str) id (str) responsible (bool): is the teacher responsible for the class grade (str) """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.name: str = self._resolver(str, "L") self.id: str = self._resolver(str, "N") self.responsible: bool = self._resolver(bool, "estResponsable") self.grade: str = self._resolver(str, "niveau", "V", "L", default="") self._client = client del self._resolver def students(self, period: Optional[Period] = None) -> List[Student]: """ Get students in the class Args: period (Optional[Period]): select a particular period (client.periods[0] by default) """ period = period or self._client.periods[0] r = self._client.post( "ListeRessources", 105, {"classe": {"N": self.id, "G": 1}, "periode": {"N": period.id, "G": 1}}, ) return [ Student(self._client, j) for j in r["dataSec"]["data"]["listeRessources"]["V"] ] class Menu(Object): """ Represents the menu of a meal Attributes: id (str) self.name (Optional[str]) date (datetime.date): the date of the menu is_lunch (bool): the menu is a lunch menu is_dinner (bool): the menu is a dinner menu first_meal (Optional[List[Food]]): food list of first meal main_meal (Optional[List[Food]]): food list of main meal side_meal (Optional[List[Food]]): food list of side meal other_meal (Optional[List[Food]]): food list of other meal cheese (Optional[List[Food]]): food list of cheese dessert (Optional[List[Food]]): food list of dessert """ class Food(Object): """ Represents food of a menu Attributes: id (str) name (str) labels (List[FoodLabel]) """ class FoodLabel(Object): """ Represents the label of a food Attributes: id (str) name (str) color (Optional[str]) """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.color: Optional[str] = self._resolver(str, "couleur", strict=False) self._client = client del self._resolver def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.labels: List[Menu.Food.FoodLabel] = self._resolver( lambda labels: [self.FoodLabel(client, label) for label in labels], "listeLabelsAlimentaires", "V", ) self._client = client del self._resolver def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.name: Optional[str] = self._resolver(str, "L", strict=False) self.date: datetime.date = self._resolver(Util.date_parse, "Date", "V") self.is_lunch: bool = self._resolver(int, "G") == 0 self.is_dinner: bool = self._resolver(int, "G") == 1 def init_food(d: dict) -> List[Menu.Food]: return [self.Food(client, x) for x in d["ListeAliments"]["V"]] d_dict = {str(meal["G"]): meal for meal in json_dict["ListePlats"]["V"]} super().__init__(d_dict) self.first_meal: Optional[List[Menu.Food]] = self._resolver( init_food, "0", strict=False ) self.main_meal: Optional[List[Menu.Food]] = self._resolver( init_food, "1", strict=False ) self.side_meal: Optional[List[Menu.Food]] = self._resolver( init_food, "2", strict=False ) self.other_meal: Optional[List[Menu.Food]] = self._resolver( init_food, "3", strict=False ) self.cheese: Optional[List[Menu.Food]] = self._resolver( init_food, "5", strict=False ) self.dessert: Optional[List[Menu.Food]] = self._resolver( init_food, "4", strict=False ) self._client = client del self._resolver class Punishment(Object): """ Represents a punishment. Attributes: id (str) given (Union[datetime.datetime, datetime.date]): Date and time when the punishment was given exclusion (bool): If the punishment is an exclusion from class during_lesson (bool): If the punishment was given during a lesson. `self.during_lesson is True => self.given is datetime.datetime` homework (Optional[str]): Text description of the homework that was given as the punishment homework_documents (List[Attachment]): Attached documents for homework circumstances (str) circumstance_documents (List[Attachment]) nature (str): Text description of the nature of the punishment (ex. "Retenue") reasons (List[str]): Text descriptions of the reasons for the punishment giver (str): Name of the person that gave the punishment schedule (List[ScheduledPunishment]): List of scheduled date-times with durations schedulable (bool) duration (Optional[datetime.timedelta]) """ class ScheduledPunishment(Object): """ Represents a sheduled punishment. Attributes: id (str) start (Union[datetime.datetime, datetime.date]) duration (Optional[datetime.timedelta]) """ def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") # construct a full datetime from "date" and "placeExecution" fields date = self._resolver(Util.date_parse, "date", "V") place = self._resolver(int, "placeExecution", strict=False) self.start: Union[datetime.datetime, datetime.date] if place is not None: liste_heures = client.func_options["dataSec"]["data"]["General"][ "ListeHeures" ]["V"] try: self.start = datetime.datetime.combine( date, Util.place2time(liste_heures, place) ) except ValueError as e: raise DataError(str(e)) else: self.start = date self.duration: Optional[datetime.timedelta] = self._resolver( lambda v: datetime.timedelta(minutes=int(v)), "duree", strict=False ) del self._resolver def __init__(self, client: ClientBase, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") date = self._resolver(Util.date_parse, "dateDemande", "V") self.during_lesson: bool = self._resolver(lambda v: not bool(v), "horsCours") # construct a full datetime from "dateDemande" and "placeDemande" fields self.given: Union[datetime.datetime, datetime.date] if self.during_lesson: time_place = self._resolver(int, "placeDemande") liste_heures = client.func_options["dataSec"]["data"]["General"][ "ListeHeures" ]["V"] try: self.given = datetime.datetime.combine( date, Util.place2time(liste_heures, time_place) ) except ValueError as e: raise DataError(str(e)) else: self.given = date self.exclusion: bool = self._resolver(bool, "estUneExclusion") self.homework: str = self._resolver(str, "travailAFaire", strict=False) self.homework_documents: List[Attachment] = self._resolver( lambda x: [Attachment(client, a) for a in x], "documentsTAF", "V", default=[], ) self.circumstances: str = self._resolver(str, "circonstances") self.circumstance_documents: List[Attachment] = self._resolver( lambda x: [Attachment(client, a) for a in x], "documentsCirconstances", "V" ) # TODO: change to an enum (out of scope for this comment: change this kind of string to enums everywhere) self.nature: str = self._resolver(str, "nature", "V", "L") self.requires_parent: Optional[str] = self._resolver( str, "nature", "V", "estAvecARParent", strict=False ) self.reasons: List[str] = self._resolver( lambda x: [i["L"] for i in x], "listeMotifs", "V" ) self.giver: str = self._resolver(str, "demandeur", "V", "L") self.schedulable: bool = self._resolver(bool, "estProgrammable") self.schedule: List[Punishment.ScheduledPunishment] = [] if self.schedulable: self.schedule = self._resolver( lambda x: [Punishment.ScheduledPunishment(client, i) for i in x], "programmation", "V", ) self.duration: Optional[datetime.timedelta] = self._resolver( lambda v: datetime.timedelta(minutes=int(v)), "duree", strict=False ) class TeachingStaff(Object): """ Represents a teaching staff member. You shouldn't have to create this class manually. Attributes: id (str): id of the teaching staff (used internally) name (str): name of the teaching staff type (str): teacher or staff num (int): the teaching staff number used for sorting subjects (List[TeachingSubject]): list of subject the teacher teaches """ class TeachingSubject(Object): """ Represents a subject taught. You shouldn't have to create this class manually. Attributes: id (str): id of the subject (used internally) name (str): name of the subject duration (Optional[datetime.timedelta]): the duration of the subject per week parent_subject_name (Optional[str]): name of the parent subject parent_subject_id (Optional[str]): id of the parent subject (used internally) """ def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self._duration: str = self._resolver(str, "volumeHoraire") self.parent_subject_name: Optional[str] = self._resolver( str, "servicePere", "V", "L", strict=False ) self.parent_subject_id: Optional[str] = self._resolver( str, "servicePere", "V", "N", strict=False ) if "h" in self._duration: # duration can be an empty string self.duration: Optional[datetime.timedelta] = datetime.timedelta( hours=int(self._duration.split("h")[0]), minutes=int(self._duration.split("h")[1]), ) else: self.duration = None del self._resolver def __init__(self, json_dict: dict) -> None: super().__init__(json_dict) self.id: str = self._resolver(str, "N") self.name: str = self._resolver(str, "L") self.num: int = self._resolver(int, "P") self._type: int = self._resolver(int, "G") self.type: str = "teacher" if self._type == 3 else "staff" self.subjects: List[TeachingStaff.TeachingSubject] = self._resolver( lambda x: [TeachingStaff.TeachingSubject(i) for i in x], "matieres", "V" ) del self._resolver