gmao/app_new/lib_ext/pronotepy/dataClasses.py
2026-08-14 16:02:25 +00:00

2162 lines
74 KiB
Python

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