zermelo-api-vogk 0.2.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ from .src.zermelo_api import ZermeloAPI
2
+ from .src.branches import Branch, Branches
3
+ from .src.time_utils import *
4
+ from .src.users import Leerling, Medewerker, Leerlingen, Personeel
5
+ from .src.lesgroepen import Lesgroepen, Lesgroep
6
+ from .src.leerjaren import Leerjaar, Leerjaren
7
+ from .src.appointments import get_user_appointments, get_department_updates, Appointment
8
+ from .src.vakken import Vakken, Vak
File without changes
@@ -0,0 +1,92 @@
1
+ from .zermelo_api import zermelo, from_zermelo_dict
2
+ from dataclasses import dataclass, InitVar, field
3
+ from logger import makeLogger, DEBUG
4
+
5
+ logger = makeLogger("APPS")
6
+
7
+
8
+ @dataclass
9
+ class Appointment:
10
+ id: int
11
+ appointmentInstance: int = 0
12
+ start: int = 0
13
+ end: int = 0
14
+ startTimeSlot: int = 0
15
+ endTimeSlot: int = 0
16
+ branch: int = 0
17
+ type: str = "unknown"
18
+ groupsInDepartments: list[int] = field(default_factory=list)
19
+ locationsOfBranch: list[int] = field(default_factory=list)
20
+ locations: list[str] = field(default_factory=list)
21
+ optional: bool = False
22
+ valid: bool = False
23
+ cancelled: bool = False
24
+ cancelledReason: str = ""
25
+ modified: bool = False
26
+ teacherChanged: bool = False
27
+ groupChanged: bool = False
28
+ locationChanged: bool = False
29
+ timeChanged: bool = False
30
+ moved: bool = False
31
+ created: int = 0
32
+ hidden: bool = False
33
+ commonSchedule: bool = False
34
+ ignoreSubstitutions: bool = False
35
+ changeDescription: str = ""
36
+ schedulerRemark: str = ""
37
+ capacity: int = 0
38
+ content: str = ""
39
+ lastModified: int = 0
40
+ new: bool = True
41
+ choosableInDepartments: list[int] = field(default_factory=list)
42
+ choosableInDepartmentCodes: list[str] = field(default_factory=list)
43
+ courses: list[int] = field(default_factory=list)
44
+ alternativeSubject: str = ""
45
+ onlineStudents: list[str] = field(default_factory=list)
46
+ extraStudentSource: str = ""
47
+ appointmentLastModified: int = 0
48
+ onlineLocationUrl: str = ""
49
+ remark: str = ""
50
+ capacityManually: bool = False
51
+ teachingTimeManually: bool = False
52
+ teachingTime: int = 0
53
+ expectedStudentCount: int = 0
54
+ expectedStudentCountOnline: int = 0
55
+ availableSpace: int = 0
56
+ udmUUID: str = ""
57
+ creator: str = ""
58
+ subjects: list[str] = field(default_factory=list)
59
+ teachers: list[int] = field(default_factory=list)
60
+ onlineTeachers: list[str] = field(default_factory=list)
61
+ students: list[str] = field(default_factory=list)
62
+
63
+ # doesn't work (not enough rights)
64
+ # @classmethod
65
+ # def get_appointment(cls, id: int):
66
+ # query = f"appointments/{id}"
67
+ # status, data = zermelo.getData(query, from_id=True)
68
+ # if status != 200:
69
+ # raise Exception(data)
70
+ # return from_zermelo_dict(cls, data[0])
71
+
72
+
73
+ def get_appointments(query: str) -> list[Appointment]:
74
+ status, data = zermelo.getData(query)
75
+ if status != 200:
76
+ raise Exception(data)
77
+ return [from_zermelo_dict(Appointment, row) for row in data]
78
+
79
+
80
+ def get_user_appointments(user: int | str, **kwargs) -> list[Appointment]:
81
+ query = f"appointments/?user={user}"
82
+ for key, val in kwargs.items():
83
+ query += f"&{key}={val}"
84
+ logger.debug(query)
85
+ return get_appointments(query)
86
+
87
+
88
+ def get_department_updates(id: int, **kwargs) -> list[Appointment]:
89
+ query = f"appointments/?containsStudentsFromGroupInDepartment={id}"
90
+ for key, val in kwargs.items():
91
+ query += f"&{key}={val}"
92
+ return get_appointments(query)
@@ -0,0 +1,93 @@
1
+ from .logger import makeLogger
2
+ from .zermelo_api import ZermeloCollection, zermelo, from_zermelo_dict
3
+ from .time_utils import get_date, get_year, datetime
4
+ from .users import Leerling, Leerlingen, Personeel, Medewerker
5
+ from .leerjaren import Leerjaren, Leerjaar
6
+ from .groepen import Groep, Groepen
7
+ from .lesgroepen import Lesgroepen, Lesgroep
8
+ from .vakken import Vakken, Vak
9
+ from dataclasses import dataclass, InitVar, field
10
+
11
+ # branch is roughly translated to 'afdeling' in Dutch
12
+ # for readability kept as branch, might be changed in the future
13
+
14
+ logger = makeLogger("BRANCH")
15
+
16
+
17
+ @dataclass
18
+ class SchoolInSchoolYear:
19
+ id: int
20
+ # school: int
21
+ # year: int
22
+ # archived: bool
23
+ # name: str
24
+ # projectName: str
25
+ # schoolName: str
26
+ # schoolHrmsCode: str
27
+
28
+
29
+ @dataclass
30
+ class Branch:
31
+ id: int
32
+ schoolInSchoolYear: int
33
+ branch: str
34
+ name: str
35
+ schoolYear: int
36
+ date: datetime = datetime.now()
37
+ leerlingen: list[Leerling] = field(default_factory=list)
38
+ personeel: list[Medewerker] = field(default_factory=list)
39
+ leerjaren: list[Leerjaar] = field(default_factory=list)
40
+ vakken: Vakken = field(default_factory=list)
41
+ groepen: Groepen = field(default_factory=list)
42
+
43
+ def __post_init__(self):
44
+ logger.info(f"*** loading branch: {self.name} ***")
45
+ self.leerlingen = Leerlingen(self.schoolInSchoolYear)
46
+ self.personeel = Personeel(self.schoolInSchoolYear)
47
+ self.leerjaren = Leerjaren(self.schoolInSchoolYear)
48
+ self.groepen = Groepen(self.schoolInSchoolYear)
49
+ self.vakken = Vakken(self.schoolInSchoolYear)
50
+
51
+ def find_lesgroepen(self) -> Lesgroepen | bool:
52
+ if self.leerlingen and self.personeel:
53
+ return Lesgroepen(
54
+ self.leerjaren,
55
+ self.vakken,
56
+ self.groepen,
57
+ self.leerlingen,
58
+ self.personeel,
59
+ )
60
+ return False
61
+
62
+
63
+ @dataclass
64
+ class Branches(ZermeloCollection, list[Branch]):
65
+ datestring: InitVar = ""
66
+
67
+ def __post_init__(self, datestring):
68
+ logger.debug("init branches")
69
+ date = get_date(datestring)
70
+ year = get_year(datestring)
71
+ logger.debug(year)
72
+ query = f"schoolsinschoolyears/?year={year}&archived=False"
73
+ data = zermelo.load_query(query)
74
+ for schoolrow in data:
75
+ school = from_zermelo_dict(SchoolInSchoolYear, schoolrow)
76
+ query = f"branchesofschools/?schoolInSchoolYear={school.id}"
77
+ self.load_collection(query, Branch)
78
+ for branch in self:
79
+ branch.date = date
80
+
81
+ def __str__(self):
82
+ return "Branches(" + ", ".join([br.name for br in self]) + ")"
83
+
84
+ def get(self, name: str) -> Branch:
85
+ logger.info(f"loading branch: {name} ")
86
+ for branch in self:
87
+ if (
88
+ name.lower() in branch.branch.lower()
89
+ or branch.branch.lower() in name.lower()
90
+ ):
91
+ return branch
92
+ else:
93
+ logger.error(f"NO Branch found for {name}")
@@ -0,0 +1,52 @@
1
+ from os import path, mkdir
2
+ from shutil import copyfile
3
+ import json
4
+
5
+
6
+ class getConfig:
7
+ def __init__(self, file):
8
+ HOMEDIR = getConfigDir()
9
+ filename = file + ".json"
10
+ backupname = file + ".bak"
11
+ if not HOMEDIR:
12
+ raise Exception("Kan configuratie directory niet aanmaken")
13
+ self.filepath = path.join(HOMEDIR, filename)
14
+ self.backup = path.join(HOMEDIR, backupname)
15
+
16
+ def __repr__(self):
17
+ return f'ConfigFile(path="{self.filepath}")'
18
+
19
+ def save(self, data):
20
+ try:
21
+ copyfile(self.filepath, self.backup)
22
+ except Exception:
23
+ print("warning - geen datafile")
24
+ try:
25
+ with open(self.filepath, "w") as file:
26
+ json.dump(data, file, indent=2)
27
+ except Exception:
28
+ return False
29
+ return True
30
+
31
+ def load(self):
32
+ data = {}
33
+ for file in [self.filepath, self.backup]:
34
+ try:
35
+ if path.isfile(file):
36
+ with open(file, "r") as json_file:
37
+ data = json.load(json_file)
38
+ except Exception:
39
+ continue
40
+ return data
41
+
42
+ def get(self, key: str = None):
43
+ data = self.load()
44
+ if not key:
45
+ return data
46
+ return data[key] if key in data else {}
47
+
48
+
49
+ def getConfigDir():
50
+ abspath = path.abspath(__file__)
51
+ dname = path.dirname(abspath)
52
+ return dname
@@ -0,0 +1,26 @@
1
+ from .config import getConfig
2
+ from .logger import makeLogger, INFO, DEBUG
3
+
4
+ logger = makeLogger("creds", INFO)
5
+
6
+
7
+ class Credentials:
8
+ def __init__(self):
9
+ self.file = getConfig("creds")
10
+ self.token = ""
11
+ self.load()
12
+
13
+ def load(self):
14
+ self.token = ""
15
+ try:
16
+ data = self.file.load()
17
+ self.token = data["token"]
18
+ except Exception as e:
19
+ logger.trace()
20
+ finally:
21
+ logger.debug(f"token: {self.token}")
22
+ return True if self.token else False
23
+
24
+ def settoken(self, token):
25
+ self.file.save({"token": token})
26
+ self.token = token
@@ -0,0 +1,35 @@
1
+ from .zermelo_api import ZermeloCollection, zermelo
2
+ from .logger import makeLogger
3
+ from dataclasses import dataclass, InitVar
4
+
5
+ logger = makeLogger("GROEPEN")
6
+
7
+
8
+ @dataclass
9
+ class Groep:
10
+ id: int
11
+ isMainGroup: bool
12
+ isMentorGroup: bool
13
+ departmentOfBranch: int
14
+ name: str
15
+ extendedName: str
16
+
17
+
18
+ @dataclass
19
+ class Groepen(ZermeloCollection, list[Groep]):
20
+ schoolinschoolyear: InitVar
21
+
22
+ def __post_init__(self, schoolinschoolyear: int):
23
+ query = f"groupindepartments?schoolInSchoolYear={schoolinschoolyear}"
24
+ self.load_collection(query, Groep)
25
+
26
+ def get_department_groups(
27
+ self, departmentOfBranch: int, maingroup: bool = False
28
+ ) -> list[Groep]:
29
+ # returns all groups (if it is/isnt stamklas (maingroup))
30
+ return [
31
+ groep
32
+ for groep in self
33
+ if groep.departmentOfBranch == departmentOfBranch
34
+ and groep.isMainGroup == maingroup
35
+ ]
@@ -0,0 +1,48 @@
1
+ from .zermelo_api import ZermeloCollection
2
+ from .logger import makeLogger
3
+ from dataclasses import dataclass, InitVar
4
+
5
+ # Leerjaar is a rough dutch translation of department in Zermelo
6
+
7
+ logger = makeLogger("Leerjaren")
8
+
9
+
10
+ def getLeerjaarNaam(string):
11
+ names = {"LG1": "klas 1", "LG2": "klas 2", "LG3": "klas 3"}
12
+ for name, result in names.items():
13
+ if name in string:
14
+ return result
15
+ else:
16
+ return string
17
+
18
+
19
+ @dataclass
20
+ class Leerjaar:
21
+ id: int
22
+ code: str
23
+ yearOfEducation: int
24
+ branchOfSchool: int
25
+ teacherTeams: list[int]
26
+ mainGroupAuthority: str
27
+ educationType: str
28
+ weekTimeTable: int
29
+ teachingLevel: str
30
+ educations: list[int]
31
+ schoolInSchoolYear: int
32
+ branchOfSchoolCode: str
33
+ schoolInSchoolYearName: str
34
+ profiles: list[int]
35
+ schoolInSchoolYearId: int
36
+ name: str = ""
37
+
38
+ def __post_init__(self):
39
+ self.name = getLeerjaarNaam(self.code.upper())
40
+
41
+
42
+ @dataclass
43
+ class Leerjaren(ZermeloCollection, list[Leerjaar]):
44
+ schoolinschoolyear: InitVar
45
+
46
+ def __post_init__(self, schoolinschoolyear: int):
47
+ query = f"departmentsofbranches?schoolInSchoolYear={schoolinschoolyear}"
48
+ self.load_collection(query, Leerjaar)
@@ -0,0 +1,238 @@
1
+ from dataclasses import dataclass, field, InitVar
2
+ from .vakken import Vakken, Vak
3
+ from .groepen import Groepen, Groep
4
+ from .users import Leerlingen, Leerling, Personeel, Medewerker
5
+ from .leerjaren import Leerjaren, Leerjaar
6
+ from .time_utils import get_date, delta_week
7
+ from .logger import makeLogger, DEBUG
8
+ from .zermelo_api import zermelo, from_zermelo_dict
9
+
10
+ # from typing import Tuple
11
+
12
+ logger = makeLogger("LESGROEP")
13
+
14
+
15
+ def createLesgroepNaam(vak: Vak, groep: Groep) -> str:
16
+ leerjaar, groepnaam = groep.extendedName.split(".")
17
+ jaarnaam = leerjaar[2:].upper()
18
+ if vak.subjectCode in groepnaam:
19
+ return f"{jaarnaam}.{groepnaam}"
20
+ else:
21
+ return f"{jaarnaam}.{vak.subjectCode}{groepnaam[-1]}"
22
+
23
+
24
+ def find_groepen(vak: Vak, groepen: Groepen) -> list[Groep]:
25
+ result = []
26
+ logger.debug(f"finding groep for vak: {vak.subjectCode}")
27
+ for groep in groepen.get_department_groups(vak.departmentOfBranch):
28
+ if groep in result:
29
+ continue
30
+ if vak.qualifiedCode and vak.qualifiedCode in groep.extendedName:
31
+ logger.debug(
32
+ f"found {groep.name} with {vak.qualifiedCode} in {groep.extendedName}"
33
+ )
34
+ result.append(groep)
35
+ return result
36
+
37
+
38
+ def clean_docs(docs: list[str]) -> list[str]:
39
+ checklist = list(set(docs))
40
+ if "lgverv" in checklist:
41
+ checklist.remove("lgverv")
42
+ max = 0
43
+ if len(checklist) > 1:
44
+ logger.warning(f"multiple docs: {checklist}")
45
+ for doc in checklist:
46
+ doc_count = docs.count(doc)
47
+ if doc_count > max:
48
+ result = doc
49
+ max = doc_count
50
+ logger.warning(f"result: {result} ({max})")
51
+ return [result]
52
+ return checklist
53
+
54
+
55
+ @dataclass
56
+ class Les:
57
+ id: int
58
+ appointmentInstance: int
59
+ teachers: list[str]
60
+ students: list[str]
61
+ subjects: list[str]
62
+ groups: list[str]
63
+ groupsInDepartments: list[int]
64
+ choosableInDepartmentCodes: list[str]
65
+ valid: bool
66
+ cancelled: bool
67
+
68
+ def filter(self, name: str) -> bool:
69
+ if self.cancelled:
70
+ return False
71
+ if not self.valid:
72
+ return False
73
+ if len(self.students) > 40:
74
+ logger.debug("groep te groot")
75
+ return False
76
+ if not any([name.split(".")[-1] in group for group in self.groups]):
77
+ logger.debug(f"{name} not in {self}")
78
+ return False
79
+ return True
80
+
81
+
82
+ def get_vak_data(
83
+ id: int, code: str, groupName: str, start, eind
84
+ ) -> tuple[list[int], list[str], list[str]]:
85
+ query = f"appointments/?containsStudentsFromGroupInDepartment={id}&subjects={code}&type=lesson&start={start}&end={eind}&fields=appointmentInstance,id,teachers,students,subjects,groups,groupsInDepartments,choosableInDepartmentCodes,valid,cancelled"
86
+ vakdata = zermelo.load_query(query)
87
+ grp_bck = []
88
+ ll_bck = []
89
+ doc_bck = []
90
+ leerlingen = []
91
+ docenten = []
92
+ grp_namen = []
93
+ lessen = [Les(**row) for row in reversed(vakdata) if row]
94
+ for les in [les for les in lessen if les.filter(groupName)]:
95
+ if len(les.groups) > 1:
96
+ if not grp_namen and not grp_bck or len(les.groups) < len(grp_bck):
97
+ logger.debug("meerdere groepen")
98
+ grp_bck = les.choosableInDepartmentCodes
99
+ ll_bck = list(set([llnr for llnr in les.students]))
100
+ doc_bck = list(set([doc for doc in les.teachers]))
101
+ continue
102
+ if les.students and les.teachers:
103
+ [leerlingen.append(llnr) for llnr in les.students if llnr not in leerlingen]
104
+ [docenten.append(doc) for doc in les.teachers]
105
+ [
106
+ grp_namen.append(grp)
107
+ for grp in les.choosableInDepartmentCodes
108
+ if grp not in grp_namen
109
+ ]
110
+ if not grp_namen and grp_bck:
111
+ logger.debug(f"result groepen: {grp_bck}")
112
+ grp_namen = grp_bck
113
+ if not docenten and doc_bck:
114
+ logger.debug(f"result docenten: {doc_bck}")
115
+ docenten = doc_bck
116
+ if not leerlingen and ll_bck:
117
+ logger.debug(f"result leerlingen: {ll_bck}")
118
+ leerlingen = ll_bck
119
+ docenten = clean_docs(docenten)
120
+ leerlingen = [int(llnr) for llnr in leerlingen]
121
+ return (leerlingen, docenten, grp_namen)
122
+
123
+
124
+ def find_deelnemers(
125
+ vak: Vak, groep: Groep
126
+ ) -> tuple[list[int], list[str], list[str]] | bool:
127
+ date = get_date()
128
+ try:
129
+ logger.debug(groep)
130
+ for x in [0, -1, -2, 1, 2, 3]:
131
+ dweek = x * 4
132
+ starttijd = int(delta_week(date, dweek).timestamp())
133
+ eindtijd = int(delta_week(date, dweek + 4).timestamp())
134
+ data = get_vak_data(
135
+ groep.id, vak.subjectCode, groep.extendedName, starttijd, eindtijd
136
+ )
137
+ leerlingen, docenten, groep_namen = data
138
+ if len(leerlingen) and len(docenten):
139
+ logger.debug(f"found for {groep}")
140
+ namen = [
141
+ groepnaam
142
+ for groepnaam in groep_namen
143
+ if vak.departmentOfBranchCode in groepnaam
144
+ ]
145
+ return (leerlingen, docenten, namen)
146
+ if not len(leerlingen) or not len(docenten):
147
+ logger.debug(f"geen deelnemers gevonden voor {groep}\n {vak}")
148
+ return False
149
+ except Exception:
150
+ logger.trace()
151
+ return False
152
+
153
+
154
+ def get_info(
155
+ llnrs: list[int],
156
+ doc_codes: list[str],
157
+ names: list[str],
158
+ ll: Leerlingen,
159
+ docs: Personeel,
160
+ ) -> tuple[list[Leerling], list[Medewerker], list[str]]:
161
+ leerlingen = [ll.get(llnr) for llnr in llnrs]
162
+ docenten = [docs.get(code) for code in doc_codes]
163
+ return (leerlingen, docenten, names)
164
+
165
+
166
+ @dataclass
167
+ class Lesgroep:
168
+ vak: Vak
169
+ groep: Groep
170
+ leerjaar: Leerjaar
171
+ leerlingen: list[Leerling] = field(default_factory=list)
172
+ docenten: list[Medewerker] = field(default_factory=list)
173
+ namen: list[str] = field(default_factory=list)
174
+ naam: str = ""
175
+
176
+ def __post_init__(self):
177
+ self.naam = createLesgroepNaam(self.vak, self.groep)
178
+ for leerling in self.leerlingen:
179
+ leerling.leerjaren.add(self.leerjaar.id)
180
+
181
+
182
+ @dataclass
183
+ class Lesgroepen(list[Lesgroep]):
184
+ leerjaren: InitVar
185
+ vakken: InitVar
186
+ groepen: InitVar
187
+ leerlingen: InitVar
188
+ personeel: InitVar
189
+
190
+ def __post_init__(
191
+ self,
192
+ leerjaren: Leerjaren,
193
+ vakken: Vakken,
194
+ groepen: Groepen,
195
+ leerlingen: Leerlingen,
196
+ personeel: Personeel,
197
+ ):
198
+ for leerjaar in leerjaren:
199
+ for vak in vakken.get_leerjaar_vakken(leerjaar.id):
200
+ if vak.subjectType in ["education", "profile"] or vak.scheduleCode in [
201
+ "lo",
202
+ "sport",
203
+ ]:
204
+ # skip educatio / profile / lo
205
+ continue
206
+ logger.debug(vak)
207
+ found = False
208
+ for groep in find_groepen(vak, groepen):
209
+ groepinfo = find_deelnemers(vak, groep)
210
+ if groepinfo:
211
+ data = get_info(*groepinfo, leerlingen, personeel)
212
+ self.append(Lesgroep(vak, groep, leerjaar, *data))
213
+ found = True
214
+ if found:
215
+ continue
216
+ logger.debug(f"trying maingroups for {vak.subjectName}")
217
+ for groep in groepen.get_department_groups(
218
+ vak.departmentOfBranch, True
219
+ ):
220
+ logger.debug(f"trying: {groep}")
221
+ groepinfo = find_deelnemers(vak, groep)
222
+ if groepinfo:
223
+ data = get_info(*groepinfo, leerlingen, personeel)
224
+ self.append(Lesgroep(vak, groep, leerjaar, *data))
225
+ found = True
226
+ if not found:
227
+ logger.warning(f"geen groepen gevonden voor {vak}")
228
+ self.clean_leerlingen()
229
+ logger.info(f"found {len(self)} lesgroepen")
230
+
231
+ def clean_leerlingen(self):
232
+ for lesgroep in self:
233
+ for leerling in lesgroep.leerlingen.copy():
234
+ if leerling.leerjaren.get_id() != lesgroep.leerjaar.id:
235
+ logger.warning(
236
+ f"removing leerling ({leerling.fullName}) from {lesgroep.naam}"
237
+ )
238
+ lesgroep.leerlingen.remove(leerling)
@@ -0,0 +1,47 @@
1
+ import logging
2
+ import logging.handlers
3
+ from logging import Logger
4
+ from logging import DEBUG, INFO, WARNING, ERROR
5
+ from os import path, mkdir
6
+ from traceback import format_exc
7
+
8
+ FOLDER = "log"
9
+ FILE = "info"
10
+
11
+
12
+ class MyLogger(Logger):
13
+ def trace(self):
14
+ self.error(format_exc())
15
+
16
+ def header(self, text: str):
17
+ self.info(f" ###### {text.upper()} ######")
18
+
19
+ def write(self, text: str):
20
+ text.replace("\n", "")
21
+ self.debug(text)
22
+
23
+
24
+ def makeLogger(name: str = None, LOG_LEVEL: int = INFO, filename=FILE) -> MyLogger:
25
+ logger = logging.getLogger(name)
26
+ logger.setLevel(LOG_LEVEL)
27
+ # if not path.isdir(FOLDER):
28
+ # mkdir(FOLDER)
29
+ # filename = path.join(FOLDER, f"{filename}.log")
30
+
31
+ # create console handler and set level to debug
32
+ # handler = logging.handlers.TimedRotatingFileHandler(
33
+ # filename, when="W4", backupCount=3, encoding="utf-8"
34
+ # )
35
+ handler = logging.StreamHandler()
36
+ handler.setLevel(LOG_LEVEL)
37
+
38
+ formatter = logging.Formatter(
39
+ "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
40
+ )
41
+ handler.setFormatter(formatter)
42
+
43
+ logger.addHandler(handler)
44
+
45
+ logger.__class__ = MyLogger
46
+
47
+ return logger
@@ -0,0 +1,18 @@
1
+ from datetime import datetime, timedelta
2
+
3
+
4
+ def get_date(datestring: str = "") -> datetime:
5
+ try:
6
+ date = datetime.strptime(datestring, "%Y-%m-%d")
7
+ except:
8
+ date = datetime.today()
9
+ finally:
10
+ return date
11
+
12
+
13
+ def delta_week(date: datetime, delta: int = 0) -> datetime:
14
+ return date + timedelta(weeks=delta)
15
+
16
+
17
+ def get_year(datestring: str = "") -> int:
18
+ return delta_week(get_date(datestring), -33).strftime("%Y")
@@ -0,0 +1,139 @@
1
+ from .zermelo_api import ZermeloCollection, zermelo, from_zermelo_dict
2
+ from .logger import makeLogger
3
+ from dataclasses import dataclass, InitVar, field
4
+
5
+ logger = makeLogger("USERS")
6
+
7
+
8
+ @dataclass
9
+ class User:
10
+ code: str
11
+ roles: list[str]
12
+ firstName: str
13
+ prefix: str
14
+ lastName: str
15
+ schoolInSchoolYears: list[int]
16
+ isApplicationManager: bool
17
+ archived: bool
18
+ isStudent: bool
19
+ isEmployee: bool
20
+ isFamilyMember: bool
21
+ hasPassword: bool
22
+ isSchoolScheduler: bool
23
+ isSchoolLeader: bool
24
+ isStudentAdministrator: bool
25
+ isTeamLeader: bool
26
+ isSectionLeader: bool
27
+ isMentor: bool
28
+ isParentTeacherNightScheduler: bool
29
+ isDean: bool
30
+ fullName: str = ""
31
+
32
+ def __post_init__(self):
33
+ self.fullName = self.generatename()
34
+
35
+ def generatename(self) -> str:
36
+ if self.prefix:
37
+ fullname = " ".join(
38
+ [self.firstName, self.prefix, self.lastName.split(",")[0]]
39
+ )
40
+ elif self.firstName is not None and self.lastName is not None:
41
+ fullname = " ".join([self.firstName, self.lastName])
42
+ elif self.lastName is not None:
43
+ fullname = self.lastName
44
+ else:
45
+ fullname = "unknown"
46
+ return fullname
47
+
48
+
49
+ class Users(list[User]):
50
+ def print_list(self):
51
+ return (
52
+ f" = [" + ", ".join([str(user) for user in self]) + "]" if len(self) else ""
53
+ )
54
+
55
+
56
+ @dataclass
57
+ class LeerjaarCounter:
58
+ id: int
59
+ count: int = 0
60
+
61
+
62
+ class LeerjaarCounters(list[LeerjaarCounter]):
63
+ def get(self, id) -> LeerjaarCounter:
64
+ for ljcounter in self:
65
+ if ljcounter.id == id:
66
+ return ljcounter
67
+ self.append(LeerjaarCounter(id))
68
+ return self[-1]
69
+
70
+ def add(self, leerjaar_id: int):
71
+ counter = self.get(leerjaar_id)
72
+ counter.count += 1
73
+
74
+ def get_id(self) -> int:
75
+ if len(self):
76
+ self.sort(key=lambda x: x.count, reverse=True)
77
+ return self[0].id
78
+ return 0
79
+
80
+
81
+ @dataclass
82
+ class Leerling(User):
83
+ volgnr: int = 0
84
+ leerjaren: LeerjaarCounters = field(default_factory=LeerjaarCounters)
85
+
86
+
87
+ @dataclass
88
+ class Leerlingen(ZermeloCollection, Users, list[Leerling]):
89
+ schoolinschoolyear: InitVar
90
+
91
+ def __init__(self, schoolinschoolyear: int):
92
+ query = f"users?schoolInSchoolYear={schoolinschoolyear}&isStudent=true"
93
+ data = zermelo.load_query(query)
94
+ if data:
95
+ self.load_leerlingen(data)
96
+
97
+ def load_leerlingen(self, data: list[dict]):
98
+ logger.info("loading Leerlingen")
99
+ data.sort(key=lambda x: (x["lastName"], x["firstName"]))
100
+ for idx, row in enumerate(data):
101
+ self.append(from_zermelo_dict(Leerling, row, volgnr=idx + 1))
102
+ logger.info(f"found: {len(self)} leerlingen")
103
+
104
+ def __repr__(self):
105
+ return f"{self}{self.print_list()}"
106
+
107
+ def __str__(self):
108
+ return f"Leerlingen({len(self)})"
109
+
110
+ def get(self, llnr) -> Leerling:
111
+ for user in self:
112
+ if user.code == str(llnr):
113
+ return user
114
+
115
+
116
+ @dataclass
117
+ class Medewerker(User):
118
+ def __repr__(self):
119
+ return self.fullName + f"({self.code})"
120
+
121
+
122
+ @dataclass
123
+ class Personeel(ZermeloCollection, Users, list[Medewerker]):
124
+ schoolinschoolyear: InitVar
125
+
126
+ def __init__(self, schoolinschoolyear: int):
127
+ query = f"users?schoolInSchoolYear={schoolinschoolyear}&isEmployee=true"
128
+ self.load_collection(query, Medewerker)
129
+
130
+ def __repr__(self):
131
+ return f"{self}{self.print_list()}"
132
+
133
+ def __str__(self):
134
+ return f"Personeel({len(self)})"
135
+
136
+ def get(self, code: str):
137
+ for user in self:
138
+ if user.code == code:
139
+ return user
@@ -0,0 +1,53 @@
1
+ from .zermelo_api import ZermeloCollection, zermelo
2
+ from .logger import makeLogger, DEBUG
3
+ from .groepen import Groepen, Groep
4
+ from dataclasses import dataclass, InitVar, field
5
+
6
+ logger = makeLogger("VAKKEN")
7
+
8
+
9
+ @dataclass
10
+ class Vak:
11
+ id: int
12
+ subject: int
13
+ departmentOfBranch: int
14
+ studentCanEdit: bool
15
+ sectionOfBranch: int
16
+ courseType: str
17
+ lessonHoursInClassPeriods: list[dict]
18
+ excludedSegments: list[int]
19
+ referenceWeek: dict # (year:int, weekNumber: int, schoolYear: int)
20
+ isExam: bool
21
+ scheduleCode: str
22
+ subjectType: str
23
+ subjectCode: str
24
+ departmentOfBranchCode: str
25
+ iltCode: int
26
+ qualifiedCode: str
27
+ subjectScheduleCode: str
28
+ subjectName: str
29
+ sectionOfBranchAbbreviation: str
30
+
31
+ def getName(self) -> str:
32
+ if "/" in self.subjectName:
33
+ logger.debug(f"old name: {self.subjectName}")
34
+ parts = self.subjectName.split("/")
35
+ frontpart = parts[0]
36
+ nameparts = frontpart.split(" ")
37
+ nameparts.pop(-1)
38
+ name = " ".join(nameparts)
39
+ logger.debug(f"new name: {name}")
40
+ return name.strip()
41
+ return self.subjectName.strip()
42
+
43
+
44
+ @dataclass
45
+ class Vakken(ZermeloCollection, list[Vak]):
46
+ schoolinschoolyear: InitVar
47
+
48
+ def __post_init__(self, schoolinschoolyear: int):
49
+ query = f"choosableindepartments?schoolInSchoolYear={schoolinschoolyear}"
50
+ self.load_collection(query, Vak)
51
+
52
+ def get_leerjaar_vakken(self, leerjaar_id: int) -> list[Vak]:
53
+ return [vak for vak in self if vak.departmentOfBranch == leerjaar_id]
@@ -0,0 +1,134 @@
1
+ from .credentials import Credentials
2
+ from .logger import makeLogger, DEBUG, INFO
3
+ import json
4
+ import requests
5
+ import inspect
6
+
7
+ logger = makeLogger("ZermeloAPI")
8
+
9
+ ZERMELO_NAME = "carmelhengelo"
10
+
11
+
12
+ class ZermeloAPI:
13
+ def __init__(self, school=ZERMELO_NAME):
14
+ self.credentials = Credentials()
15
+ self.zerurl = f"https://{school}.zportal.nl/api/v3/"
16
+
17
+ def login(self, code: str) -> bool:
18
+ token = self.get_access_token(code)
19
+ return self.add_token(token)
20
+
21
+ def get_access_token(self, code: str) -> str:
22
+ token = ""
23
+ url = self.zerurl + "oauth/token"
24
+ # headers = {"Content-Type": "application/json"}
25
+ zerrequest = requests.post(
26
+ url, data={"grant_type": "authorization_code", "code": code}
27
+ )
28
+ if zerrequest.status_code == 200:
29
+ data = json.loads(zerrequest.text)
30
+ if "access_token" in data:
31
+ token = data["access_token"]
32
+ return token
33
+
34
+ def add_token(self, token: str) -> bool:
35
+ if not token:
36
+ return False
37
+ self.credentials.settoken(token)
38
+ return self.checkCreds()
39
+
40
+ def checkCreds(self):
41
+ result = False
42
+ try:
43
+ self.getName()
44
+ result = True
45
+ except Exception as e:
46
+ logger.trace()
47
+ logger.error(e)
48
+ finally:
49
+ return result
50
+
51
+ def getName(self):
52
+ if not self.credentials.token:
53
+ raise Exception("No Token loaded!")
54
+ status, data = self.getData("users/~me", True)
55
+ if status != 200 or not len(data):
56
+ raise Exception("could not load user data with token")
57
+ logger.debug(f"get name: {data[0]}")
58
+ row = data[0]
59
+ if not row["prefix"]:
60
+ return " ".join([row["firstName"], row["lastName"]])
61
+ else:
62
+ return " ".join([row["firstName"], row["prefix"], row["lastName"]])
63
+
64
+ def getData(self, task, from_id=False) -> tuple[int, list[dict] | str]:
65
+ result = (500, [])
66
+ try:
67
+ request = (
68
+ self.zerurl + task + f"?access_token={self.credentials.token}"
69
+ if from_id
70
+ else self.zerurl + task + f"&access_token={self.credentials.token}"
71
+ )
72
+ logger.debug(request)
73
+ json_response = requests.get(request).json()
74
+ if json_response:
75
+ json_status = json_response["response"]["status"]
76
+ if json_status == 200:
77
+ result = (200, json_response["response"]["data"])
78
+ logger.debug(" **** JSON OK ****")
79
+ else:
80
+ logger.debug(
81
+ f"oeps, geen juiste response: {task} - {json_response['response']}"
82
+ )
83
+ result = (json_status, json_response["response"])
84
+ else:
85
+ logger.error("JSON - response is leeg")
86
+ except Exception as e:
87
+ logger.trace()
88
+ finally:
89
+ return result
90
+
91
+ def load_query(self, query: str) -> list[dict]:
92
+ try:
93
+ status, data = self.getData(query)
94
+ if status != 200:
95
+ raise Exception(f"Error loading data {status}")
96
+ if not data:
97
+ logger.debug("no data")
98
+ except Exception as e:
99
+ logger.debug(e)
100
+ data = []
101
+ return data
102
+
103
+
104
+ zermelo = ZermeloAPI()
105
+
106
+ if not zermelo.checkCreds():
107
+ with open("creds.ini") as f:
108
+ token = f.read()
109
+ zermelo.add_token(token)
110
+
111
+
112
+ def from_zermelo_dict(cls, data: dict, *args, **kwargs):
113
+ [
114
+ logger.debug(f"{k} ({v}) not defined in {cls}")
115
+ for k, v in data.items()
116
+ if k not in inspect.signature(cls).parameters
117
+ ]
118
+ return cls(
119
+ *args,
120
+ **{k: v for k, v in data.items() if k in inspect.signature(cls).parameters},
121
+ **kwargs,
122
+ )
123
+
124
+
125
+ class ZermeloCollection:
126
+ def load_collection(self: list, query: str, type: object) -> None:
127
+ data = zermelo.load_query(query)
128
+ for row in data:
129
+ self.append(from_zermelo_dict(type, row))
130
+
131
+ def test(self: list, query: str):
132
+ data = zermelo.load_query(query)
133
+ for row in data:
134
+ logger.info(f"test: {row}")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Klaas Vogel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.1
2
+ Name: zermelo-api-vogk
3
+ Version: 0.2.16
4
+ Summary: A small module to create a Zermelo accesstoken and put some data from Zermelo in dataclasses
5
+ Home-page: https://github.com/KlaasVogel/zermelo_api_vogk
6
+ Author: Klaas Vogel
7
+ Author-email: zermelo_api@klaasvogel.nl
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests
16
+ Provides-Extra: dev
17
+
18
+ ## zermelo_api_vogk
19
+ A small module to create a Zermelo accesstoken and put some data from Zermelo in dataclasses
20
+
21
+ # V0.2.16
22
+ - found bug when running as daemon, probably: logger.
23
+ - try without output to file
24
+
25
+ # V0.2.15
26
+ - disabled debug mode
27
+
28
+ # V0.2.14
29
+ - debug on for bugfixing
30
+ - added extra debug log for filter in lesgroepen
31
+ - edit filter to find second part of groupname in les.groups
32
+
33
+ # V0.2.13
34
+ - New bug: loading of stamgroepen as lesgroep is broken!
35
+ - need to update/edit filter in lesgroepen
36
+
37
+ # V0.2.12
38
+ - removed debug for wisa and lesgroepen
39
+
40
+ # V0.2.11
41
+ - added groep.extendentName to filter in lesgroep
42
+
43
+ # V0.2.10
44
+ - trying to optimize lesgroepen
45
+ - added (temp) debug to lesgroepen
46
+ - log group
47
+
48
+ # V0.2.9
49
+ - removed dependency of Groepen in Vakken (ot used)
50
+ - added getName to Vak (Vakken)
51
+ - start cleanup code
52
+
53
+ # V0.2.8
54
+ - changed definition of Vakken and Groepen in Branch
55
+ - added Vakken & Vak to __init__.py
56
+
57
+ # V0.2.7
58
+ - added response as data in getData on wrong output
59
+ - refactoring of Appointments
60
+ * added function: get_department_updates [doesn't work atm :( ]
61
+
62
+ # V0.2.6
63
+ - removed linebreaks from logger.write()
64
+
65
+ # V0.2.5
66
+ - trying getting extra data for each appointment from user
67
+ - changed "with_id" in get_data to "from_id" and reversed functionality for better readability
68
+ - added classmethod to Appointment to get Appointment direct from id
69
+
70
+ # V0.2.4
71
+ - added getting appointments
72
+
73
+ # V0.2.3
74
+ - changed getting lesgroepen only as return so not part of branch anymore
75
+ - removed lastcheck from lesgroepen
76
+
77
+ # V0.2.2
78
+ - bugfix lesgroepen
79
+ - added write to logger (debug)
80
+ - changed repr of Users
81
+
82
+ # V0.2.1
83
+ - added datetime to each branch
84
+ - init of branches is now only datestring, year will be determined internally
85
+
86
+ # V0.2.0
87
+ - changed decorator from_zermelo_dict to function
88
+
89
+ # V0.1.3
90
+ - added lesgroepen
91
+
92
+ # V0.1.2
93
+ - added Vakken
94
+
95
+ # V0.1.1
96
+ - added groepen / groups
97
+ - First setup of vakken
98
+ - cleanup of code
99
+
100
+ # V0.1.0: First Refactoring already:
101
+ - deleted fields.py and moved usefull functions to zermelo_api
102
+ - loading of data for (zermelo)list generalized in zermelo_api
103
+
104
+ # V0.0.8:
105
+ - Added loading of Users (Leerlingen & Medewerkers)
106
+ - Added backtracker to MyLogger
107
+
108
+ # V0.0.7:
109
+ - Added Loading of Branches
110
+
111
+ # V0.0.1 -> V0.0.5:
112
+ - Experimenting with creating of own package and major structure of project
@@ -0,0 +1,19 @@
1
+ zermelo_api/__init__.py,sha256=judeG_xciHkFfKbkcM7S29LFaey0ljdwC7tnpo61TX0,401
2
+ zermelo_api/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ zermelo_api/src/appointments.py,sha256=zHl41LsYWpJIaaHD1zTjgeKZXaNinTs9PxvKsbAzfLw,3019
4
+ zermelo_api/src/branches.py,sha256=BlhsqaKPjE8hb5LPeZwiW3pXqS1AoZeNyaF2JVM5YCI,3023
5
+ zermelo_api/src/config.py,sha256=_NMg7DVl0YvrsDblV8128TxNkWvHylIT8vFiqP6FwqI,1437
6
+ zermelo_api/src/credentials.py,sha256=rfgOatC6yY4iq8Fc7DboJ2tnlcZjcxsbQEAVt_VpaC0,644
7
+ zermelo_api/src/groepen.py,sha256=2HWDQxXgQaHYCJEFCmXJJH-qEB7mzeM3XLziGzKTT0M,941
8
+ zermelo_api/src/leerjaren.py,sha256=2h8SZBOj9bm0w2mYL2T8rMbRfCoIkDib_6ElfkdaNR4,1216
9
+ zermelo_api/src/lesgroepen.py,sha256=4o5C0q2ZfipkLnO9wpME-slYZFLTr5WEZvHNxFlRdl8,8417
10
+ zermelo_api/src/logger.py,sha256=QEKNK6TrDDrLPtXnW1hPbJIIjhmKwps1eFCJaanJjHE,1202
11
+ zermelo_api/src/time_utils.py,sha256=WAPhjZdaYFk3a5gtJGtH_Rmc2ckbQqpA_wMkzekdoOo,446
12
+ zermelo_api/src/users.py,sha256=eus_ZwvEfmqWAG7cC7Si4-qUsUvBF-cLv_PkhIiIs-s,3696
13
+ zermelo_api/src/vakken.py,sha256=ffYOjSi9f80ezCJeWjbQMc5fQAWAzTZTz6NayHuOdzc,1594
14
+ zermelo_api/src/zermelo_api.py,sha256=QsDHBa0y3h8jdSHQCB7hgFWh_Z0IStIBBizwc6cqJHs,4256
15
+ zermelo_api_vogk-0.2.16.dist-info/LICENSE,sha256=-g7wJAy8hK6L6CqC6eT8eGsW8t-6Ej8xK4pToiooumg,1068
16
+ zermelo_api_vogk-0.2.16.dist-info/METADATA,sha256=Vh6gL8dGflyYBW2OSV5AR9XPzQfUT_1y5AAeywvlx2I,2922
17
+ zermelo_api_vogk-0.2.16.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
18
+ zermelo_api_vogk-0.2.16.dist-info/top_level.txt,sha256=pDeE62UsvIU1PZdHVaksaQb3tkB5hvXla-bnHkLY-XA,12
19
+ zermelo_api_vogk-0.2.16.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ zermelo_api