MagisterPy 0.1.0__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.
- MagisterPy/__init__.py +4 -0
- MagisterPy/error_handler.py +27 -0
- MagisterPy/jsparser.py +43 -0
- MagisterPy/magister_errors.py +23 -0
- MagisterPy/magister_session.py +342 -0
- MagisterPy/request_manager.py +169 -0
- MagisterPy-0.1.0.dist-info/LICENSE +21 -0
- MagisterPy-0.1.0.dist-info/METADATA +57 -0
- MagisterPy-0.1.0.dist-info/RECORD +11 -0
- MagisterPy-0.1.0.dist-info/WHEEL +5 -0
- MagisterPy-0.1.0.dist-info/top_level.txt +1 -0
MagisterPy/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .magister_errors import *
|
|
3
|
+
def error_handler(func):
|
|
4
|
+
'''
|
|
5
|
+
A decorator used to handle errors that can occure when when executing the functions
|
|
6
|
+
'''
|
|
7
|
+
|
|
8
|
+
def wrapper(*args, **kwargs):
|
|
9
|
+
_self = args[0]
|
|
10
|
+
try:
|
|
11
|
+
result = func(*args,**kwargs)
|
|
12
|
+
return result
|
|
13
|
+
except KeyboardInterrupt:
|
|
14
|
+
raise KeyboardInterrupt
|
|
15
|
+
|
|
16
|
+
except requests.exceptions.ConnectionError as e:
|
|
17
|
+
_self._logMessage("Could not connect to Magister")
|
|
18
|
+
if _self.automatically_handle_errors:
|
|
19
|
+
pass
|
|
20
|
+
else:
|
|
21
|
+
raise ConnectionError()
|
|
22
|
+
except BaseMagisterError as e:
|
|
23
|
+
if _self.automatically_handle_errors:
|
|
24
|
+
_self._logMessage(e.message)
|
|
25
|
+
else:
|
|
26
|
+
raise e
|
|
27
|
+
return wrapper
|
MagisterPy/jsparser.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
class JsParser():
|
|
4
|
+
def __init__(self):
|
|
5
|
+
pass
|
|
6
|
+
def get_authcode_from_js(self,js_content:str):
|
|
7
|
+
line = 1192
|
|
8
|
+
column = 15476
|
|
9
|
+
buffer = 200
|
|
10
|
+
authcode = ""
|
|
11
|
+
js_content = js_content.split("\n")[line][column:column+buffer]
|
|
12
|
+
start_list_index = None
|
|
13
|
+
content_list = [] #stores the 2 lists containing info about the authcode
|
|
14
|
+
|
|
15
|
+
#Find the first and the second list
|
|
16
|
+
for idx, _char in enumerate(js_content):
|
|
17
|
+
|
|
18
|
+
if _char == "[":
|
|
19
|
+
start_list_index = idx
|
|
20
|
+
|
|
21
|
+
if _char == "]" and (not (start_list_index is None)):
|
|
22
|
+
|
|
23
|
+
content_list.append(json.loads(js_content[start_list_index:idx+1]))
|
|
24
|
+
|
|
25
|
+
if len(content_list)>1:
|
|
26
|
+
break
|
|
27
|
+
|
|
28
|
+
if len(content_list) == 2:
|
|
29
|
+
convert_to_int = lambda a: int(a)
|
|
30
|
+
|
|
31
|
+
random_char_list, index_list = content_list
|
|
32
|
+
|
|
33
|
+
index_list = list(map(convert_to_int,index_list))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
for idx in index_list:
|
|
37
|
+
authcode+= random_char_list[idx]
|
|
38
|
+
if len(authcode) == 8:
|
|
39
|
+
return authcode
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class BaseMagisterError(Exception):
|
|
2
|
+
|
|
3
|
+
def __init__(self, message=None):
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
self.message = message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnableToInputCredentials(BaseMagisterError):
|
|
9
|
+
|
|
10
|
+
def __init__(self, message="\nCouldn't input the credentials\nThis error can occure if the credentials were not Input in order\nSchool->Username->Passwords"):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IncorrectCredentials(BaseMagisterError):
|
|
16
|
+
def __init__(self, message="\nThe credentials provided were either incorrect or Magister rejected them"):
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.message = message
|
|
19
|
+
|
|
20
|
+
class ConnectionError(BaseMagisterError):
|
|
21
|
+
def __init__(self, message="\nCould not connect to Magister. Please check your internet connection"):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from urllib.parse import urlparse, parse_qs
|
|
3
|
+
from .request_manager import LoginRequestsSender
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from .error_handler import error_handler
|
|
6
|
+
from .magister_errors import *
|
|
7
|
+
|
|
8
|
+
class MagisterSession():
|
|
9
|
+
'''
|
|
10
|
+
Creates a session with Magister
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
enable_logging (bool): Used to display errors in the standard output
|
|
14
|
+
|
|
15
|
+
automatically_handle_errors (bool): Used to automatically handle errors. If any function fails it returns None instead of raising an error
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
def __init__(self, enable_logging = False, automatically_handle_errors = True):
|
|
19
|
+
|
|
20
|
+
self.request_sender = LoginRequestsSender()
|
|
21
|
+
self.session = requests.Session()
|
|
22
|
+
self.profile_auth_token = None #auth token for the redirect page
|
|
23
|
+
self.app_auth_token = None #auth token for the main app
|
|
24
|
+
self.authcode = None # gets randomly generated once every 3-7 days
|
|
25
|
+
self.sessionid = None # gets assigned when entering the login page
|
|
26
|
+
self.returnurl = None # used for some requests
|
|
27
|
+
self.main_payload = None #payload containing common parameters (authcode, returnurl, sessionid)
|
|
28
|
+
self.person_id = None # your account's person_id
|
|
29
|
+
self.account_id = None #your account id
|
|
30
|
+
self.api_url = None #url for accessing magister API
|
|
31
|
+
self.x_correlation_id = None #used in login requests
|
|
32
|
+
self.automatically_handle_errors = automatically_handle_errors
|
|
33
|
+
|
|
34
|
+
self.recieve_log = enable_logging
|
|
35
|
+
def _logMessage(self,msg:str):
|
|
36
|
+
if self.recieve_log:
|
|
37
|
+
print(msg)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@error_handler
|
|
41
|
+
def input_school(self, school_name:str) ->Optional[requests.Response]:
|
|
42
|
+
'''
|
|
43
|
+
Sets up a session by inputting the school name. This is the **first step** in the login sequence
|
|
44
|
+
and must be called before `input_username()` and `input_password()`.
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
school_name (str): The name of the school to authenticate against.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
This method is the **first step** of the login process. It must be called before
|
|
51
|
+
`input_username()` and `input_password()` to initialize the session.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
requests.Response: if the school is found and session is successfully initiated.
|
|
55
|
+
None: if the school is not found or there’s an issue in the school lookup.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
session.input_school("MySchoolName")
|
|
59
|
+
'''
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
#Initializing the login session
|
|
63
|
+
url_login_page = "https://accounts.magister.net/"
|
|
64
|
+
|
|
65
|
+
response = self.session.get(url_login_page,allow_redirects=False)
|
|
66
|
+
|
|
67
|
+
redirect_url_1 = r"https://accounts.magister.net/connect/authorize?client_id=iam-profile&redirect_uri=https%3A%2F%2Faccounts.magister.net%2Fprofile%2Foidc%2Fredirect_callback.html&response_type=id_token%20token&scope=openid%20profile%20email%20magister.iam.profile&state=57dcb9c3b667407791ff32a7af41e703&nonce=ec78d557c0e44751bf573db6719445cd"
|
|
68
|
+
response = self.session.get(redirect_url_1,allow_redirects=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
response = self.session.get(response.headers.get("location"), allow_redirects= False)
|
|
72
|
+
|
|
73
|
+
response = self.session.get("https://accounts.magister.net/" + response.headers.get("location"), allow_redirects= False)
|
|
74
|
+
|
|
75
|
+
self.sessionid = parse_qs(urlparse(response.url).query).get('sessionId', [None])[0]
|
|
76
|
+
self.returnurl = parse_qs(urlparse(response.url).query).get('returnUrl', [None])[0]
|
|
77
|
+
self.x_correlation_id = parse_qs(urlparse(self.returnurl).query).get('X-Correlation-ID', [None])[0]
|
|
78
|
+
|
|
79
|
+
javascript_redirect_url = self.request_sender.extract_redirect_url_from_html(response.text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
response = self.session.get(f"https://accounts.magister.net/{javascript_redirect_url}")
|
|
83
|
+
|
|
84
|
+
self.authcode = self.request_sender.extract_dynamic_authcode(response.text)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
self.main_payload = {
|
|
89
|
+
'authCode': self.authcode,
|
|
90
|
+
'returnUrl': self.returnurl,
|
|
91
|
+
'sessionId': self.sessionid
|
|
92
|
+
}
|
|
93
|
+
#Inputting the credentials
|
|
94
|
+
|
|
95
|
+
#school
|
|
96
|
+
try:
|
|
97
|
+
response = self.request_sender.set_school(request_session=self.session,school_name=school_name,main_payload=self.main_payload)
|
|
98
|
+
if response.status_code !=200:
|
|
99
|
+
raise IncorrectCredentials(f"Could not find school: {school_name}")
|
|
100
|
+
except requests.exceptions.JSONDecodeError:
|
|
101
|
+
raise IncorrectCredentials(f"Could not find school: {school_name}")
|
|
102
|
+
|
|
103
|
+
return response
|
|
104
|
+
@error_handler
|
|
105
|
+
def input_username(self,username:str) -> Optional[requests.Response]:
|
|
106
|
+
'''
|
|
107
|
+
Sets the username for the current session. This is the **second step** in the login sequence
|
|
108
|
+
and must be called after `input_school()` but before `input_password()`.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
username (str): The username for the account.
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
This function must be called **after `input_school()`** and **before `input_password()`** to
|
|
115
|
+
authenticate the username.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
requests.Response: if the username is accepted.
|
|
119
|
+
None: if the username is not found or if there’s an issue during authentication.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
session.input_username("myusername")
|
|
123
|
+
'''
|
|
124
|
+
|
|
125
|
+
if not self.main_payload:
|
|
126
|
+
raise UnableToInputCredentials()
|
|
127
|
+
response = self.request_sender.set_username(request_session=self.session,username=username,main_payload=self.main_payload)
|
|
128
|
+
if response.status_code != 200:
|
|
129
|
+
raise IncorrectCredentials()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
@error_handler
|
|
134
|
+
def input_password(self,password:str) -> Optional[requests.Response]:
|
|
135
|
+
'''
|
|
136
|
+
Sets the password for the session and finalizes the login process. This is the **third and final step**
|
|
137
|
+
in the login sequence and must be called after `input_school()` and `input_username()`.
|
|
138
|
+
|
|
139
|
+
Upon success, this method retrieves and stores essential session variables like `profile_auth_token`,
|
|
140
|
+
`api_url`, `app_auth_token`, `account_id`, and `person_id` for further interactions with the API.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
password (str): The password associated with the username.
|
|
144
|
+
|
|
145
|
+
Usage:
|
|
146
|
+
This function is the **final step** in the login process and should only be called **after
|
|
147
|
+
`input_school()` and `input_username()`**.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
requests.Response: if the password is correct and login is successful.
|
|
151
|
+
None: if the password is incorrect or if there’s a login cooldown.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
session.input_password("mypassword")
|
|
155
|
+
'''
|
|
156
|
+
if not self.main_payload:
|
|
157
|
+
raise UnableToInputCredentials()
|
|
158
|
+
response = self.request_sender.set_password(request_session=self.session,password=password,main_payload=self.main_payload)
|
|
159
|
+
if response.status_code !=200:
|
|
160
|
+
raise IncorrectCredentials("Incorrect password or the password input is on cooldown")
|
|
161
|
+
|
|
162
|
+
#setup for variables
|
|
163
|
+
self.profile_auth_token = self.request_sender.get_profile_auth_token(request_session=self.session)
|
|
164
|
+
self.api_url = self.request_sender.get_api_url(request_session=self.session,profile_auth_token=self.profile_auth_token)
|
|
165
|
+
self.app_auth_token = self.request_sender.get_app_auth_token(request_session=self.session,api_url=self.api_url)
|
|
166
|
+
self.account_id = self.request_sender.get_accountid(request_session=self.session,app_auth_token=self.app_auth_token,api_url=self.api_url)
|
|
167
|
+
self.person_id = self.request_sender.get_personid(request_session=self.session,app_auth_token=self.app_auth_token,api_url=self.api_url,account_id=self.account_id)
|
|
168
|
+
|
|
169
|
+
self._logMessage("you have successfully logged in!")
|
|
170
|
+
return response
|
|
171
|
+
@error_handler
|
|
172
|
+
def login(self,school_name:str,username:str,password:str) -> bool:
|
|
173
|
+
'''
|
|
174
|
+
logs the user into their account
|
|
175
|
+
|
|
176
|
+
returns:
|
|
177
|
+
True -> if user logged in successfully
|
|
178
|
+
False -> if user wasn't able to login
|
|
179
|
+
|
|
180
|
+
params:
|
|
181
|
+
school_name -> a string of school name (not case sensitive. It sets the first school from the list that magister provides)
|
|
182
|
+
username -> a string of a username (should be exact)
|
|
183
|
+
password -> a string with the password of the user (should be exact)
|
|
184
|
+
'''
|
|
185
|
+
#Inputting the school
|
|
186
|
+
input_school_response = self.input_school(school_name=school_name)
|
|
187
|
+
|
|
188
|
+
if not input_school_response:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#username
|
|
193
|
+
input_username_response = self.input_username(username=username)
|
|
194
|
+
if not input_username_response:
|
|
195
|
+
return False
|
|
196
|
+
#password
|
|
197
|
+
input_password_response = self.input_password(password=password)
|
|
198
|
+
if not input_password_response:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@error_handler
|
|
205
|
+
def get_schedule(self, _from:str, to:str,with_changes = False) -> list[dict]:
|
|
206
|
+
'''
|
|
207
|
+
Retrieves the user’s schedule within a specified date range.
|
|
208
|
+
|
|
209
|
+
This method fetches all scheduled items between two dates, starting from `_from` to `to`.
|
|
210
|
+
The session must be authenticated by calling `.login()` first.
|
|
211
|
+
|
|
212
|
+
Parameters:
|
|
213
|
+
- _from (str): Start date of the schedule period in "YYYY-MM-DD" format.
|
|
214
|
+
- to (str): End date of the schedule period in "YYYY-MM-DD" format.
|
|
215
|
+
- with_changes: Sends a different requests which retrieves recent changes. (It's not getting the changes on specific dates specifically so it's recomended to use with_changes = False instead)
|
|
216
|
+
Returns:
|
|
217
|
+
- list[dict]: A list of dictionaries representing schedule items, each with detailed fields.
|
|
218
|
+
The schedule items are sorted chronologically from earliest to latest.
|
|
219
|
+
|
|
220
|
+
Structure of Each Schedule Item:
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"Start": "datetime", # Start time of the scheduled item
|
|
224
|
+
"Einde": "datetime", # End time of the scheduled item
|
|
225
|
+
"LesuurVan": bool, # Boolean for lesson period start
|
|
226
|
+
"LesuurTotMet": bool, # Boolean for lesson period end
|
|
227
|
+
"DuurtHeleDag": bool, # Whether the event lasts all day
|
|
228
|
+
"Omschrijving": str, # Description of the event
|
|
229
|
+
"Lokatie": str, # Location of the event
|
|
230
|
+
"Status": int, # Status code of the event
|
|
231
|
+
"Type": int, # Type identifier of the event
|
|
232
|
+
"Subtype": int, # Subtype identifier of the event
|
|
233
|
+
"IsOnlineDeelname": bool, # Whether online attendance is allowed
|
|
234
|
+
"WeergaveType": int, # Display type identifier
|
|
235
|
+
"Inhoud": str, # Content or details about the event
|
|
236
|
+
"Opmerking": str, # Additional notes
|
|
237
|
+
"InfoType": int, # Information type identifier
|
|
238
|
+
"Aantekening": str, # Notes or annotations
|
|
239
|
+
"Afgerond": bool, # Whether the event is completed
|
|
240
|
+
"HerhaalStatus": int, # Repeat status identifier
|
|
241
|
+
"Herhaling": None, # Repeat information (usually null)
|
|
242
|
+
"Vakken": list[dict], # List of subjects related to the event
|
|
243
|
+
"Docenten": list[dict], # List of teachers associated with the event
|
|
244
|
+
"Lokalen": list[dict], # List of rooms assigned for the event
|
|
245
|
+
"Groepen": None, # Group information (usually null)
|
|
246
|
+
"OpdrachtId": int, # Task ID if associated
|
|
247
|
+
"HeeftBijlagen": bool, # Whether attachments are available
|
|
248
|
+
"Bijlagen": None # Attachment information (usually null)
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Example Usage:
|
|
253
|
+
```python
|
|
254
|
+
session.get_schedule(_from="2024-11-10", to="2024-11-11")
|
|
255
|
+
```
|
|
256
|
+
'''
|
|
257
|
+
if not self.app_auth_token:
|
|
258
|
+
self._logMessage("You have not logged in yet")
|
|
259
|
+
return
|
|
260
|
+
#Returns a schedule with no changes in it
|
|
261
|
+
remove_links_and_id = lambda a: {k: v for k, v in a.items() if k not in ["Links", "Id"]}
|
|
262
|
+
params = {
|
|
263
|
+
"status" : 1,
|
|
264
|
+
"tot": to,
|
|
265
|
+
"van": _from
|
|
266
|
+
}
|
|
267
|
+
headers = {"authorization":self.app_auth_token}
|
|
268
|
+
if not with_changes:
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
url = f"{self.api_url}/personen/{self.person_id}/afspraken"
|
|
273
|
+
respone = self.session.get(url=url,params=params,headers=headers)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
del params["status"]
|
|
278
|
+
url = f"{self.api_url}/personen/{self.person_id}/roosterwijzigingen"
|
|
279
|
+
respone = self.session.get(url=url,params=params,headers=headers)
|
|
280
|
+
|
|
281
|
+
if respone.status_code == 200:
|
|
282
|
+
|
|
283
|
+
response_json = respone.json()["Items"]
|
|
284
|
+
return list(map(remove_links_and_id,response_json))
|
|
285
|
+
@error_handler
|
|
286
|
+
def get_grades(self, top:int = 25,skip:int = 0) -> list[dict]:
|
|
287
|
+
'''
|
|
288
|
+
Retrieves the most recent grades for the user.
|
|
289
|
+
|
|
290
|
+
This method fetches the latest grades for the authenticated user.
|
|
291
|
+
The session must be authenticated by calling `.login()` first.
|
|
292
|
+
|
|
293
|
+
Parameters:
|
|
294
|
+
- top (int): Number of grades to retrieve (default is 25).
|
|
295
|
+
- skip (int): Number of grades to skip, for pagination (default is 0).
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
- list[dict]: A list of dictionaries, each representing a grade item with relevant details.
|
|
299
|
+
Grades are sorted from the most recent to the oldest.
|
|
300
|
+
|
|
301
|
+
Structure of Each Grade Item:
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"omschrijving": str, # Description of the grade item
|
|
305
|
+
"ingevoerdOp": "datetime", # Date when the grade was entered
|
|
306
|
+
"vak": { # Subject information
|
|
307
|
+
"code": str, # Subject code
|
|
308
|
+
"omschrijving": str # Subject description
|
|
309
|
+
},
|
|
310
|
+
"waarde": str, # Grade value or score
|
|
311
|
+
"weegfactor": float, # Weight factor of the grade
|
|
312
|
+
"isVoldoende": bool, # Whether the grade is sufficient
|
|
313
|
+
"teltMee": bool, # Whether the grade counts in the final score
|
|
314
|
+
"moetInhalen": bool, # If the grade needs to be retaken
|
|
315
|
+
"heeftVrijstelling": bool, # If the grade has an exemption
|
|
316
|
+
"behaaldOp": None, # Date achieved (if available)
|
|
317
|
+
"links": dict # Additional links or references (usually empty)
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Example Usage:
|
|
322
|
+
```python
|
|
323
|
+
session.get_grades(top=1)
|
|
324
|
+
```
|
|
325
|
+
'''
|
|
326
|
+
if not self.app_auth_token:
|
|
327
|
+
self._logMessage("You have not logged in yet")
|
|
328
|
+
return
|
|
329
|
+
remove_id = lambda a: {k: v for k, v in a.items() if k not in ["kolomId"]}
|
|
330
|
+
params = {
|
|
331
|
+
"top": top,
|
|
332
|
+
"skip": skip
|
|
333
|
+
}
|
|
334
|
+
headers = {"authorization":self.app_auth_token}
|
|
335
|
+
url = f"{self.api_url}/personen/{self.person_id}/cijfers/laatste"
|
|
336
|
+
respone = self.session.get(url=url,params=params,headers=headers)
|
|
337
|
+
|
|
338
|
+
if respone.status_code == 200:
|
|
339
|
+
|
|
340
|
+
response_json = respone.json()["items"]
|
|
341
|
+
return list(map(remove_id,response_json))
|
|
342
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from urllib.parse import urlparse, parse_qs
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
from .jsparser import *
|
|
5
|
+
from typing import Optional
|
|
6
|
+
class LoginRequestsSender():
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def get_subdomain(self,url):
|
|
14
|
+
|
|
15
|
+
parsed_url = urlparse(url)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
netloc = parsed_url.netloc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
parts = netloc.split('.')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if len(parts) > 2:
|
|
25
|
+
|
|
26
|
+
return '.'.join(parts[:-2])
|
|
27
|
+
else:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_accountid(self,request_session:requests.Session, app_auth_token,api_url) -> str:
|
|
32
|
+
url = f"{api_url}/sessions/current"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
headers = {
|
|
36
|
+
"authorization": app_auth_token
|
|
37
|
+
}
|
|
38
|
+
response = request_session.get(url, headers=headers,cookies=request_session.cookies)
|
|
39
|
+
if response.status_code == 200:
|
|
40
|
+
response_link = response.json()["links"]["account"]["href"]
|
|
41
|
+
account_id = response_link[response_link.rfind("/")+1:]
|
|
42
|
+
|
|
43
|
+
return account_id
|
|
44
|
+
|
|
45
|
+
def get_personid(self,request_session:requests.Session, app_auth_token,api_url,account_id) -> str:
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
url = f"{api_url}/accounts/{account_id}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
headers = {
|
|
53
|
+
"authorization": app_auth_token
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
response = request_session.get(url=url,headers=headers)
|
|
57
|
+
|
|
58
|
+
if response.status_code == 200:
|
|
59
|
+
response_link = response.json()["links"]["leerling"]["href"]
|
|
60
|
+
personid = response_link[response_link.rfind("/")+1:]
|
|
61
|
+
|
|
62
|
+
return personid
|
|
63
|
+
def extract_auth_token(self,url) -> str:
|
|
64
|
+
# Parse the URL to get the fragment
|
|
65
|
+
parsed_url = urlparse(url)
|
|
66
|
+
|
|
67
|
+
fragment = parsed_url.fragment
|
|
68
|
+
|
|
69
|
+
# Parse the fragment to get the id_token
|
|
70
|
+
token_params = parse_qs(fragment)
|
|
71
|
+
id_token = token_params.get('access_token')
|
|
72
|
+
|
|
73
|
+
if id_token:
|
|
74
|
+
return id_token[0] # Return the first id_token found
|
|
75
|
+
else:
|
|
76
|
+
return None
|
|
77
|
+
def get_profile_auth_token(self,request_session:requests.Session) -> str:
|
|
78
|
+
url = r"https://accounts.magister.net/connect/authorize?client_id=iam-profile&redirect_uri=https%3A%2F%2Faccounts.magister.net%2Fprofile%2Foidc%2Fredirect_callback.html&response_type=id_token%20token&scope=openid%20profile%20email%20magister.iam.profile&state=57dcb9c3b667407791ff32a7af41e703&nonce=ec78d557c0e44751bf573db6719445cd"
|
|
79
|
+
response = request_session.get(url,allow_redirects=False)
|
|
80
|
+
|
|
81
|
+
url = response.headers["Location"]
|
|
82
|
+
return "Bearer " + self.extract_auth_token(url)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_app_auth_token(self,request_session:requests.Session,api_url) -> str:
|
|
86
|
+
url = "https://accounts.magister.net/connect/authorize"
|
|
87
|
+
subdomain = self.get_subdomain(api_url)
|
|
88
|
+
# Parameters as a dictionary
|
|
89
|
+
params = {
|
|
90
|
+
"client_id": f"M6-{subdomain}.magister.net",
|
|
91
|
+
"redirect_uri": f"https://{subdomain}.magister.net/oidc/redirect_callback.html",
|
|
92
|
+
"response_type": "id_token token",
|
|
93
|
+
"scope": "openid profile opp.read opp.manage attendance.overview attendance.administration "
|
|
94
|
+
"calendar.user calendar.ical.user calendar.to-do.user grades.read grades.manage "
|
|
95
|
+
"oso.administration registration.admin lockers.administration enrollment.admin",
|
|
96
|
+
"state": "57dcb9c3b667407791ff32a7af41e703",
|
|
97
|
+
"nonce": "ec78d557c0e44751bf573db6719445cd",
|
|
98
|
+
"acr_values": f"tenant:{subdomain}.magister.net"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
response = request_session.get(url, params=params,allow_redirects=False)
|
|
102
|
+
url = response.headers["Location"]
|
|
103
|
+
return "Bearer " + self.extract_auth_token(url)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_api_url(self,request_session:requests.Session,profile_auth_token) -> str:
|
|
107
|
+
headers = {"authorization": profile_auth_token}
|
|
108
|
+
|
|
109
|
+
response = request_session.get("https://magister.net/.well-known/host-meta.json",headers=headers)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
items = response.json()
|
|
113
|
+
main_page = items["links"][0]["href"]
|
|
114
|
+
return main_page
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def search_for_tenant_id(self,request_session:requests.Session,school_name,session_id) ->str :
|
|
118
|
+
response = request_session.get(f"https://accounts.magister.net/challenges/tenant/search?sessionId={session_id}&key={school_name}")
|
|
119
|
+
return response.json()[0]["id"]
|
|
120
|
+
def set_school(self,request_session:requests.Session,school_name,main_payload) ->requests.Response:
|
|
121
|
+
|
|
122
|
+
tenant_id = self.search_for_tenant_id(request_session,school_name, main_payload["sessionId"])
|
|
123
|
+
main_payload["tenant"] = tenant_id
|
|
124
|
+
|
|
125
|
+
response= self.send_post_request(request_session,main_payload,"https://accounts.magister.net/challenges/tenant")
|
|
126
|
+
return response
|
|
127
|
+
|
|
128
|
+
def set_password(self,request_session,password,main_payload) ->requests.Response:
|
|
129
|
+
main_payload["password"] = password
|
|
130
|
+
main_payload["userWantsToPairSoftToken"] = False
|
|
131
|
+
return self.send_post_request(request_session, main_payload,"https://accounts.magister.net/challenges/password")
|
|
132
|
+
|
|
133
|
+
def set_username(self,request_session,username,main_payload) -> requests.Response:
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
main_payload["username"] = username
|
|
137
|
+
response = self.send_post_request(request_session,main_payload,"https://accounts.magister.net/challenges/username")
|
|
138
|
+
return response
|
|
139
|
+
def send_post_request(self,request_session:requests.Session,payload,url,auth_token = None) -> requests.Response:
|
|
140
|
+
|
|
141
|
+
headers = {
|
|
142
|
+
"accept": "application/json",
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
"authorization": auth_token,
|
|
145
|
+
"origin": "https://accounts.magister.net",
|
|
146
|
+
"x-xsrf-token": request_session.cookies.get("XSRF-TOKEN")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
response = request_session.post(url=url,json=payload,headers=headers, cookies=request_session.cookies)
|
|
153
|
+
return response
|
|
154
|
+
def extract_redirect_url_from_html(self,html_content:str) ->str:
|
|
155
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
156
|
+
|
|
157
|
+
# Find the script tag with defer attribute
|
|
158
|
+
script_tag = soup.find('script', {'defer': 'defer'})
|
|
159
|
+
|
|
160
|
+
# Extract the src attribute
|
|
161
|
+
if script_tag and 'src' in script_tag.attrs:
|
|
162
|
+
script_src = script_tag['src']
|
|
163
|
+
return script_src
|
|
164
|
+
else:
|
|
165
|
+
return None
|
|
166
|
+
def extract_dynamic_authcode(self,js_content):
|
|
167
|
+
return JsParser().get_authcode_from_js(js_content=js_content)
|
|
168
|
+
|
|
169
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 H3LL0U
|
|
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,57 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: MagisterPy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package for retrieving information from magister
|
|
5
|
+
Home-page: https://github.com/H3LL0U/MagisterPy
|
|
6
|
+
Author: H3LL0U
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: beautifulsoup4==4.12.3
|
|
14
|
+
Requires-Dist: certifi==2024.8.30
|
|
15
|
+
Requires-Dist: charset-normalizer==3.4.0
|
|
16
|
+
Requires-Dist: idna==3.10
|
|
17
|
+
Requires-Dist: requests==2.32.3
|
|
18
|
+
Requires-Dist: soupsieve==2.6
|
|
19
|
+
Requires-Dist: urllib3==2.2.3
|
|
20
|
+
|
|
21
|
+
# MagisterPY
|
|
22
|
+
|
|
23
|
+
This library will help you interact with your magister account using python!
|
|
24
|
+
|
|
25
|
+
## Disclaimer:
|
|
26
|
+
Please note: Using unauthorized APIs to access Magister is against Magister’s Terms of Service.
|
|
27
|
+
By using this library, you assume all responsibility and accept any risks associated with breaching these terms.
|
|
28
|
+
For more details, please refer to Magister's Terms of Service. (https://magister.nl/over-ons/juridische-zaken/)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
```
|
|
32
|
+
pip install git+https://github.com/H3LL0U/MagisterPy.git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Contributing
|
|
36
|
+
Feel free to create an issue if something doesn't work. It's only been tested on a singular school so far so it is to be expected. If you want to help add a feature it would be great as well! :D
|
|
37
|
+
|
|
38
|
+
## Basic usage
|
|
39
|
+
The following code snippet demonstrates how to create a session, log in, and retrieve your schedule and recent grades:
|
|
40
|
+
```
|
|
41
|
+
from magisterpy import MagisterSession
|
|
42
|
+
|
|
43
|
+
# Create a new session and log in
|
|
44
|
+
session = MagisterSession()
|
|
45
|
+
session.login(school_name="School_name", username="your_username", password="your_password")
|
|
46
|
+
|
|
47
|
+
# Get schedule for a specific date range
|
|
48
|
+
my_schedule = session.get_schedule("2024-11-03", "2024-11-10")
|
|
49
|
+
|
|
50
|
+
# Get the most recent grade
|
|
51
|
+
my_most_recent_grade = session.get_grades(top=1)[0]["waarde"]
|
|
52
|
+
|
|
53
|
+
print("Schedule:", my_schedule)
|
|
54
|
+
print("Most Recent Grade:", my_most_recent_grade)
|
|
55
|
+
```
|
|
56
|
+
With MagisterPy, you can access and manage your Magister account directly from Python, automating repetitive tasks and integrating your school data into your projects. We hope you find it helpful!
|
|
57
|
+
More functionality to come!
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MagisterPy/__init__.py,sha256=pBqUptTl7dfnWfMw3LSyzfLF3YELq25SGxtVt9oe4OI,152
|
|
2
|
+
MagisterPy/error_handler.py,sha256=CnK60mfbn6VtFG4wD5Nt7afFMkSfqawYRl15ZM5RpWw,872
|
|
3
|
+
MagisterPy/jsparser.py,sha256=IkRqD_NcwXTE7CgeNP2OKpUOmnm2cLvEXYuFVYpPrpo,1283
|
|
4
|
+
MagisterPy/magister_errors.py,sha256=jFXMc1dSwMep6JYMx4ZdsmQRzTSuW3mdIhSfToZ0eQY,897
|
|
5
|
+
MagisterPy/magister_session.py,sha256=mFIDwNcYWS9dU0Qfw3IBzZjQKVKkpK1Jx0wWFiBDNG4,15829
|
|
6
|
+
MagisterPy/request_manager.py,sha256=V5lIgs1GoLMoCMx-qEOVmIS5W6klWX8YqJ9ugnsbkqo,6697
|
|
7
|
+
MagisterPy-0.1.0.dist-info/LICENSE,sha256=beoj0kwBYTN1yTItpASt3XIMGW0KX4rcbJOVpy2mZ6c,1084
|
|
8
|
+
MagisterPy-0.1.0.dist-info/METADATA,sha256=0pLOSEu9ojJw7phtlWx7SzON0QF1Jaxyp1IW0zJ40c4,2278
|
|
9
|
+
MagisterPy-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
10
|
+
MagisterPy-0.1.0.dist-info/top_level.txt,sha256=-8fWbu8ze-lHyvaI77aZo1gQkd8KNU5vvZBPkLLum-4,11
|
|
11
|
+
MagisterPy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MagisterPy
|