2162 lines
74 KiB
Python
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
|