ladok3 4.13__py3-none-any.whl → 5.4__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.
ladok3/api.nw CHANGED
@@ -16,13 +16,11 @@ We will use the following to test the API methods.
16
16
  <<test api.py>>=
17
17
  import json
18
18
  import ladok3
19
+ import ladok3.cli
19
20
  import os
20
21
 
21
- ladok = ladok3.LadokSession(
22
- os.environ["LADOK_INST"],
23
- vars={"username": os.environ["LADOK_USER"],
24
- "password": os.environ["LADOK_PASS"]},
25
- test_environment=True) # for experiments
22
+ ladok = ladok3.LadokSession(*ladok3.cli.load_credentials(),
23
+ test_environment=True) # for experiments
26
24
 
27
25
  student_uid = "de709f81-a867-11e7-8dbf-78e86dc2470c"
28
26
  dasak_instance_id = "39c56d6a-73d8-11e8-b4e0-063f9afb40e3"
@@ -41,13 +39,11 @@ This is useful for development and when LADOK changes anything in the API.
41
39
  \begin{pycode}[apitest]
42
40
  import json
43
41
  import ladok3
42
+ import ladok3.cli
44
43
  import os
45
44
 
46
- ladok = ladok3.LadokSession(
47
- os.environ["LADOK_INST"],
48
- vars={"username": os.environ["LADOK_USER"],
49
- "password": os.environ["LADOK_PASS"]},
50
- test_environment=True) # for experiments
45
+ ladok = ladok3.LadokSession(*ladok3.cli.load_credentials(),
46
+ test_environment=True) # for experiments
51
47
 
52
48
  student_uid = "de709f81-a867-11e7-8dbf-78e86dc2470c"
53
49
  dasak_instance_id = "39c56d6a-73d8-11e8-b4e0-063f9afb40e3"
@@ -59,6 +55,142 @@ KTH_org_id = "2474f616-dc41-11e8-8cc1-eaeeb71b497f"
59
55
  \end{pycode}
60
56
 
61
57
 
58
+ \section{Exception classes}\label{ExceptionClasses}
59
+
60
+ LADOK operations can fail in various ways, and it's important to handle these
61
+ failures gracefully with meaningful error messages. To provide better error
62
+ handling and debugging capabilities, we define a hierarchy of custom exception
63
+ classes that represent different types of errors that can occur when working
64
+ with the LADOK API.
65
+
66
+ All custom exceptions inherit from a base [[LadokError]] class, which in turn
67
+ inherits from Python's built-in [[Exception]] class. This ensures backward
68
+ compatibility while providing enhanced error handling capabilities.
69
+
70
+ \subsection{Exception class hierarchy}
71
+
72
+ The exception hierarchy is designed to allow applications to catch specific
73
+ types of errors while still maintaining the ability to catch all LADOK-related
74
+ errors with a single exception handler.
75
+
76
+ \begin{description}
77
+ \item[{[[LadokError]]}] Base exception class for all LADOK-related errors.
78
+ \item[{[[LadokServerError]]}] For server-side LADOK errors (uses
79
+ [[response.json()["Meddelande"]]]).
80
+ \item[{[[LadokValidationError]]}] For data validation errors (invalid person
81
+ numbers, grades, etc.).
82
+ \item[{[[LadokAPIError]]}] For API/HTTP related errors (network failures,
83
+ unexpected responses).
84
+ \item[{[[LadokNotFoundError]]}] For resource not found errors (missing courses,
85
+ components, etc.).
86
+ \end{description}
87
+
88
+ Usage example:
89
+ \begin{minted}{python}
90
+ import ladok3
91
+ import ladok3.cli
92
+
93
+ try:
94
+ session = ladok3.LadokSession(**ladok3.cli.load_credentials(),
95
+ test_environment=True)
96
+ student = session.get_student("invalid-person-nr")
97
+ except ladok3.LadokValidationError as e:
98
+ print(f"Invalid data provided: {e}")
99
+ except ladok3.LadokServerError as e:
100
+ print(f"LADOK server returned error: {e}")
101
+ except ladok3.LadokAPIError as e:
102
+ print(f"Network/API failure: {e}")
103
+ except ladok3.LadokError as e:
104
+ print(f"General LADOK error: {e}")
105
+ \end{minted}
106
+ This example should yield at least a validation error, since we supply an
107
+ invalid personnummer to [[.get_student]].
108
+
109
+ \subsection{Exception class definitions}
110
+
111
+ The exception classes are defined as follows:
112
+ <<classes>>=
113
+ class LadokError(Exception):
114
+ """
115
+ Base exception class for all LADOK-related errors.
116
+
117
+ This is the parent class for all LADOK-specific exceptions. It can be used
118
+ to catch any LADOK-related error in a single exception handler while still
119
+ allowing more specific error handling when needed.
120
+
121
+ All other LADOK exception classes inherit from this class, ensuring
122
+ backward compatibility with existing code that catches generic exceptions.
123
+ """
124
+ pass
125
+
126
+ class LadokServerError(LadokError):
127
+ """
128
+ Exception for server-side LADOK errors.
129
+
130
+ This exception is raised when the LADOK server returns an error response,
131
+ typically containing an error message in the response JSON under the
132
+ "Meddelande" key. This indicates that the request reached the server
133
+ successfully, but the server was unable to process it due to business
134
+ logic constraints or data validation issues on the server side.
135
+
136
+ Examples:
137
+ - Attempting to register a grade for a student not enrolled in the course
138
+ - Trying to access data that the authenticated user doesn't have permission to view
139
+ - Server-side validation failures
140
+ """
141
+ pass
142
+
143
+ class LadokValidationError(LadokError):
144
+ """
145
+ Exception for data validation errors.
146
+
147
+ This exception is raised when input data fails validation before being
148
+ sent to the LADOK API. This includes format validation, business rule
149
+ validation, and other client-side checks that can be performed without
150
+ contacting the server.
151
+
152
+ Examples:
153
+ - Invalid Swedish personal numbers (personnummer)
154
+ - Invalid grade values for a specific grading scale
155
+ - Missing required fields
156
+ - Date format errors
157
+ """
158
+ pass
159
+
160
+ class LadokAPIError(LadokError):
161
+ """
162
+ Exception for API/HTTP related errors.
163
+
164
+ This exception is raised when there are problems with the HTTP communication
165
+ to the LADOK API or when the API returns unexpected responses. This includes
166
+ network connectivity issues, HTTP protocol errors, and malformed responses.
167
+
168
+ Examples:
169
+ - Network connectivity failures
170
+ - HTTP status codes indicating client or server errors
171
+ - Malformed JSON responses
172
+ - Authentication failures
173
+ - Timeouts
174
+ """
175
+ pass
176
+
177
+ class LadokNotFoundError(LadokError):
178
+ """
179
+ Exception for resource not found errors.
180
+
181
+ This exception is raised when a requested resource cannot be found in LADOK.
182
+ This is typically used for HTTP 404 responses or when searching for entities
183
+ that don't exist.
184
+
185
+ Examples:
186
+ - Course instances that don't exist
187
+ - Students not found in the system
188
+ - Course components that are not part of a course
189
+ - Non-existent course rounds
190
+ """
191
+ pass
192
+ @
193
+
62
194
  \section{HTTP queries to LADOK}
63
195
 
64
196
  We will make all queries to LADOK over HTTP as they provide a REST API.
@@ -78,7 +210,7 @@ To run against the test environment, change the base URL to
78
210
  \end{center}
79
211
  <<LadokSession constructor body>>=
80
212
  self.base_url = "https://www.start.ladok.se" if not test_environment \
81
- else "https://www.test.ladok.se"
213
+ else "https://www.test.ladok.se"
82
214
  self.base_gui_url = self.base_url + "/gui"
83
215
  self.base_gui_proxy_url = self.base_gui_url + "/proxy"
84
216
  @
@@ -90,9 +222,12 @@ application/vnd.ladok-kataloginformation+json, \
90
222
  application/vnd.ladok-studentinformation+json, \
91
223
  application/vnd.ladok-studiedeltagande+json, \
92
224
  application/vnd.ladok-utbildningsinformation+json, \
93
- application/vnd.ladok-examen+json, application/vnd.ladok-extintegration+json, \
94
- application/vnd.ladok-uppfoljning+json, application/vnd.ladok-extra+json, \
95
- application/json, text/plain' }
225
+ application/vnd.ladok-examen+json, \
226
+ application/vnd.ladok-extintegration+json, \
227
+ application/vnd.ladok-uppfoljning+json, \
228
+ application/vnd.ladok-extra+json, \
229
+ application/json, \
230
+ text/plain'}
96
231
  @
97
232
 
98
233
 
@@ -107,51 +242,147 @@ freshness of the XSRF token.
107
242
  The other requests track that by the use of the XSRF token itself.
108
243
  We'll get back to this in \cref{XSRFtoken}.
109
244
  <<LadokSession data methods>>=
110
- def get_query(self, path, content_type="application/vnd.ladok-resultat+json"):
111
- """Returns GET query response for path on the LADOK server"""
245
+ def get_query(self, path,
246
+ content_type="application/vnd.ladok-resultat+json"):
247
+ """
248
+ Make GET query to LADOK server and return JSON data.
249
+
250
+ Args:
251
+ path: API endpoint path
252
+ content_type: HTTP Content-Type header value
253
+
254
+ Returns:
255
+ JSON data from the response
256
+
257
+ Raises:
258
+ LadokServerError: If the server returns an error message
259
+ LadokAPIError: If the request fails or returns an error status
260
+ """
112
261
  headers = self.headers.copy()
113
262
  headers["Content-Type"] = content_type
114
263
 
115
264
  <<record time of request>>
116
265
 
117
- return self.session.get(
266
+ response = self.session.get(
118
267
  url=self.base_gui_proxy_url + path,
119
268
  headers=headers)
269
+
270
+ if response.ok:
271
+ return response.json()
272
+ try:
273
+ error_msg = response.json()["Meddelande"]
274
+ raise LadokServerError(error_msg)
275
+ except:
276
+ error_msg = response.text
277
+ raise LadokAPIError(f"GET request to {path} failed: {error_msg}")
120
278
 
121
279
  def put_query(self, path, put_data,
122
- content_type="application/vnd.ladok-resultat+json"):
123
- """Returns PUT query response for path on the LADOK server"""
280
+ content_type="application/vnd.ladok-resultat+json"):
281
+ """
282
+ Make PUT query to LADOK server and return JSON data.
283
+
284
+ Args:
285
+ path: API endpoint path
286
+ put_data: Data to send in request body
287
+ content_type: HTTP Content-Type header value
288
+
289
+ Returns:
290
+ JSON data from the response
291
+
292
+ Raises:
293
+ LadokServerError: If the server returns an error message
294
+ LadokAPIError: If the request fails or returns an error status
295
+ """
124
296
  headers = self.headers.copy()
125
297
  headers["Content-Type"] = content_type
126
298
  headers["X-XSRF-TOKEN"] = self.xsrf_token
127
299
  headers["Referer"] = self.base_gui_url
128
300
 
129
- return self.session.put(
301
+ response = self.session.put(
130
302
  url=self.base_gui_proxy_url + path,
131
303
  json=put_data,
132
304
  headers=headers)
305
+
306
+ if response.ok:
307
+ return response.json()
308
+ try:
309
+ error_msg = response.json()["Meddelande"]
310
+ raise LadokServerError(error_msg)
311
+ except:
312
+ error_msg = response.text
313
+ raise LadokAPIError(f"PUT request to {path} failed: {error_msg}")
133
314
 
134
315
  def post_query(self, path, post_data,
135
- content_type="application/vnd.ladok-resultat+json"):
136
- """Returns POST query response for path on the LADOK server"""
316
+ content_type="application/vnd.ladok-resultat+json"):
317
+ """
318
+ Make POST query to LADOK server and return JSON data.
319
+
320
+ Args:
321
+ path: API endpoint path
322
+ post_data: Data to send in request body
323
+ content_type: HTTP Content-Type header value
324
+
325
+ Returns:
326
+ JSON data from the response
327
+
328
+ Raises:
329
+ LadokServerError: If the server returns an error message
330
+ LadokAPIError: If the request fails or returns an error status
331
+ """
137
332
  headers = self.headers.copy()
138
333
  headers["Content-Type"] = content_type
139
334
  headers["X-XSRF-TOKEN"] = self.xsrf_token
140
335
  headers["Referer"] = self.base_gui_url
141
336
 
142
- return self.session.post(
337
+ response = self.session.post(
143
338
  url=self.base_gui_proxy_url + path,
144
339
  json=post_data,
145
340
  headers=headers)
341
+
342
+ if response.ok:
343
+ return response.json()
344
+ try:
345
+ error_msg = response.json()["Meddelande"]
346
+ raise LadokServerError(error_msg)
347
+ except:
348
+ error_msg = response.text
349
+ raise LadokAPIError(f"POST request to {path} failed: {error_msg}")
146
350
 
147
351
  def del_query(self, path):
148
- """Returns GET query response for path on the LADOK server"""
352
+ """
353
+ Returns DELETE query response for path on the LADOK server.
354
+
355
+ Args:
356
+ path (str): API endpoint path
357
+
358
+ Returns:
359
+ True, if success but no content is returned.
360
+ JSON data from the response, if any.
361
+ Otherwise the response object.
362
+
363
+ Raises:
364
+ LadokServerError: If the server returns an error message
365
+ LadokAPIError: If the request fails or returns an error status
366
+ """
149
367
  headers = self.headers.copy()
150
368
  headers["X-XSRF-TOKEN"] = self.xsrf_token
151
369
 
152
- return self.session.delete(
153
- url=self.base_gui_proxy_url + path,
154
- headers=headers)
370
+ response = self.session.delete(url=self.base_gui_proxy_url+path,
371
+ headers=headers)
372
+
373
+ if response.status_code == requests.codes.no_content:
374
+ return True
375
+ if response.ok:
376
+ try:
377
+ return response.json()
378
+ except:
379
+ return response
380
+ try:
381
+ error_msg = response.json()["Meddelande"]
382
+ raise LadokServerError(error_msg)
383
+ except:
384
+ error_msg = response.text
385
+ raise LadokAPIError(f"DELETE request to {path} failed: {error_msg}")
155
386
  @
156
387
 
157
388
  \subsection{The XSRF token}\label{XSRFtoken}
@@ -166,6 +397,15 @@ invalidated by LADOK.
166
397
  <<LadokSession data methods>>=
167
398
  @property
168
399
  def xsrf_token(self):
400
+ """
401
+ Get a fresh XSRF token for authenticated requests.
402
+
403
+ LADOK requires XSRF tokens for PUT, POST, and DELETE requests. This property
404
+ ensures the token is fresh and valid, triggering a re-login if necessary.
405
+
406
+ Returns:
407
+ str: A valid XSRF token for use in authenticated requests.
408
+ """
169
409
  <<ensure the XSRF token is fresh>>
170
410
 
171
411
  cookies = self.session.cookies.get_dict()
@@ -195,8 +435,8 @@ POST or DEL request will soon follow.
195
435
  Hence, we can update the time of the last request whenever the XSRF token is
196
436
  read.
197
437
  <<ensure the XSRF token is fresh>>=
198
- if not self.__access_time \
199
- or datetime.datetime.now()-self.__access_time > self.__timeout:
438
+ if (not self.__access_time
439
+ or datetime.datetime.now()-self.__access_time > self.__timeout):
200
440
  self.user_info_JSON() # trigger login
201
441
  else:
202
442
  <<record time of request>>
@@ -212,6 +452,15 @@ These recursively transcends the JSON structure removing the data that should
212
452
  be removed.
213
453
  <<functions>>=
214
454
  def clean_data(json_obj):
455
+ """
456
+ Clean a JSON object by removing internal links and pseudonymizing personal data.
457
+
458
+ Args:
459
+ json_obj (dict or list): The JSON data to clean.
460
+
461
+ Returns:
462
+ dict or list: The cleaned JSON object.
463
+ """
215
464
  remove_links(json_obj)
216
465
  pseudonymize(json_obj)
217
466
  return json_obj
@@ -221,7 +470,9 @@ The [[remove_links]] functions removes the [[link]] key--value pairs.
221
470
  The [[link]] values contains URLs for all requests that data are based on.
222
471
  <<functions>>=
223
472
  def remove_links(json_obj):
224
- """Recursively removes all "link" keys and values"""
473
+ """
474
+ Recursively removes all "link" keys and values
475
+ """
225
476
  if isinstance(json_obj, dict):
226
477
  if "link" in json_obj:
227
478
  json_obj.pop("link")
@@ -236,7 +487,9 @@ The [[pseudonymize]] function replaces names and personnummer with dummy
236
487
  entries.
237
488
  <<functions>>=
238
489
  def pseudonymize(json_obj):
239
- """Recursively pseudonymizes a JSON data record"""
490
+ """
491
+ Recursively pseudonymizes a JSON data record
492
+ """
240
493
  if isinstance(json_obj, dict):
241
494
  if "Fornamn" in json_obj:
242
495
  json_obj["Fornamn"] = "Student"
@@ -272,13 +525,26 @@ To request the grading scales from LADOK, we request all of them and return a
272
525
  list of JSON data objects containing the grading scale data.
273
526
  <<LadokSession data methods>>=
274
527
  def grade_scales_JSON(self):
275
- response = self.get_query(
528
+ """
529
+ Fetch all grading scales from LADOK.
530
+
531
+ Returns:
532
+ list: A list of JSON objects containing grading scale data from LADOK.
533
+
534
+ Raises:
535
+ Exception: If the request fails or returns an error status.
536
+ """
537
+ data = self.get_query(
276
538
  "/kataloginformation/internal/grunddata/betygsskala",
277
- content_type="application/vnd.ladok-kataloginformation+json;charset=UTF-8")
539
+ content_type="application/vnd.ladok-kataloginformation+json;charset=UTF-8"
540
+ )
278
541
 
279
- if response.status_code == requests.codes.ok:
280
- return response.json()["Betygsskala"]
281
- raise Exception(f"can't fetch grading scales: {response.text}")
542
+ try:
543
+ return data["Betygsskala"]
544
+ except KeyError as err:
545
+ err.add_note(f"Response data: {data}")
546
+ raise LadokAPIError(f"Unexpected response format when fetching grading scales: "
547
+ f"missing 'Betygsskala' key") from err
282
548
  @
283
549
 
284
550
  We add the following test.
@@ -322,9 +588,23 @@ of Chip.)
322
588
  #
323
589
  # RETURNERAR en dictionary med för- och efternamn and more
324
590
  def get_student_data_JSON(self, person_nr_raw, lang = 'sv'):
591
+ """
592
+ Get student data from LADOK using a Swedish personal number (personnummer).
593
+
594
+ Args:
595
+ person_nr_raw (str): Swedish personal number in format YYYYMMDD-XXXX or similar
596
+ lang (str, optional): Language code 'en' or 'sv'. Defaults to 'sv'.
597
+
598
+ Returns:
599
+ dict: Student data dictionary containing name, contact info, and more.
600
+
601
+ Raises:
602
+ Exception: If the personal number format is invalid.
603
+ ValueError: If student cannot be found or multiple matches found.
604
+ """
325
605
  person_nr = format_personnummer(person_nr_raw)
326
606
 
327
- if not person_nr: raise Exception('Invalid person nr ' + person_nr_raw)
607
+ if not person_nr: raise LadokValidationError('Invalid person nr ' + person_nr_raw)
328
608
 
329
609
  response = self.session.get(
330
610
  url=self.base_gui_proxy_url +
@@ -367,13 +647,23 @@ directly.
367
647
  #
368
648
  # RETURNERAR en dictionary med för- och efternamn and more
369
649
  def get_student_data_by_uid_JSON(self, uid):
370
- response = self.get_query(
650
+ """
651
+ Get student data from LADOK using a LADOK UID.
652
+
653
+ Args:
654
+ uid (str): The student's unique LADOK identifier.
655
+
656
+ Returns:
657
+ dict: Student data dictionary containing personal information.
658
+
659
+ Raises:
660
+ AttributeError: If student cannot be found by the given UID.
661
+ """
662
+ data = self.get_query(
371
663
  f"/studentinformation/internal/student/{uid}",
372
664
  content_type="application/vnd.ladok-studentinformation+json;charset=UTF-8")
373
665
 
374
- if response.status_code == requests.codes.ok:
375
- return response.json()
376
- raise AttributeError(f"can't fetch student attributes by LADOK ID {uid}")
666
+ return data
377
667
  @
378
668
 
379
669
  To test this function, we do the following.
@@ -398,15 +688,20 @@ This data includes email, postal address and phone number.
398
688
  It also includes when they were last updated.
399
689
  <<LadokSession data methods>>=
400
690
  def get_student_contact_data_JSON(self, student_id):
401
- """Returns contact data for student with student_id, returns JSON"""
402
- response = self.get_query(
403
- f"/studentinformation/internal/student/{student_id}/kontaktuppgifter",
404
- "application/vnd.ladok-studentinformation+json")
405
-
406
- if response.status_code == requests.codes.ok:
407
- return response.json()
408
- raise Exception("can't get contact data for "
409
- f"student {student_id}: {response.text}")
691
+ """
692
+ Returns contact data for student with student_id, returns JSON
693
+ """
694
+ try:
695
+ return self.get_query(
696
+ f"/studentinformation/internal/student/{student_id}/kontaktuppgifter",
697
+ "application/vnd.ladok-studentinformation+json"
698
+ )
699
+ except LadokAPIError as err:
700
+ raise LadokAPIError(f"Failed to get contact data for "
701
+ f"student {student_id}: {err}") from err
702
+ except LadokServerError as err:
703
+ raise LadokServerError(f"LADOK server error when getting contact data for "
704
+ f"student {student_id}: {err}") from err
410
705
  @
411
706
 
412
707
  We test this function.
@@ -434,14 +729,17 @@ def get_student_suspensions_JSON(self, student_id):
434
729
  Returns suspensions from studies for student with student_id,
435
730
  returns JSON
436
731
  """
437
- response = self.get_query(
438
- f"/studentinformation/internal/avstangning/student/{student_id}",
439
- "application/vnd.ladok-studentinformation+json")
440
-
441
- if response.status_code == requests.codes.ok:
442
- return response.json()
443
- raise Exception("can't get suspensions for "
444
- f"student {student_id}: {response.text}")
732
+ try:
733
+ return self.get_query(
734
+ f"/studentinformation/internal/avstangning/student/{student_id}",
735
+ "application/vnd.ladok-studentinformation+json"
736
+ )
737
+ except LadokAPIError as err:
738
+ raise LadokAPIError(f"Failed to get suspensions for "
739
+ f"student {student_id}: {err}") from err
740
+ except LadokServerError as err:
741
+ raise LadokServerError(f"LADOK server error when getting suspensions for "
742
+ f"student {student_id}: {err}") from err
445
743
  @
446
744
 
447
745
  We test this function.
@@ -466,16 +764,21 @@ This methods returns \emph{all} registrations for a student, \ie registrations
466
764
  on courses and programmes.
467
765
  <<LadokSession data methods>>=
468
766
  def registrations_JSON(self, student_id):
469
- """Return all registrations for student with ID student_id."""
470
- response = self.get_query(
767
+ """
768
+ Return all registrations for student with ID student_id.
769
+ """
770
+ data = self.get_query(
471
771
  "/studiedeltagande/internal/tillfallesdeltagande/kurstillfallesdeltagande"
472
772
  f"/student/{student_id}",
473
- "application/vnd.ladok-studiedeltagande+json")
773
+ "application/vnd.ladok-studiedeltagande+json"
774
+ )
474
775
 
475
- if response.status_code == requests.codes.ok:
476
- return response.json()["Tillfallesdeltaganden"]
477
- raise Exception("can't get registrations for "
478
- f"student {student_id}: {response.text}")
776
+ try:
777
+ return data["Tillfallesdeltaganden"]
778
+ except KeyError as err:
779
+ err.add_note(f"Response data: {data}")
780
+ raise LadokAPIError(f"Unexpected response format when fetching registrations for "
781
+ f"student {student_id}: missing 'Tillfallesdeltaganden' key") from err
479
782
  @
480
783
 
481
784
  We provide the following test.
@@ -502,20 +805,24 @@ student.
502
805
  This way we can check if a student has been registered several times on a
503
806
  course.
504
807
  <<LadokSession data methods>>=
505
- def registrations_on_course_JSON(self,
506
- course_education_id, student_id):
507
- """Return a list of registrations on course with education_id for student
508
- with student_id. JSON format."""
509
- response = self.get_query(
808
+ def registrations_on_course_JSON(self, course_education_id, student_id):
809
+ """
810
+ Return a list of registrations on course with education_id for student with
811
+ student_id. JSON format.
812
+ """
813
+ data = self.get_query(
510
814
  "/studiedeltagande/internal/tillfallesdeltagande"
511
815
  f"/utbildning/{course_education_id}/student/{student_id}",
512
- "application/vnd.ladok-studiedeltagande+json")
816
+ "application/vnd.ladok-studiedeltagande+json"
817
+ )
513
818
 
514
- if response.status_code == requests.codes.ok:
515
- return response.json()["Tillfallesdeltaganden"]
516
- raise Exception("can't get registrations for "
517
- f"student {student_id} on course {course_education_id}: "
518
- f"{response.text}")
819
+ try:
820
+ return data["Tillfallesdeltaganden"]
821
+ except KeyError as err:
822
+ err.add_note(f"Response data: {data}")
823
+ raise LadokAPIError(f"Unexpected response format when fetching registrations for "
824
+ f"student {student_id} on course {course_education_id}: "
825
+ f"missing 'Tillfallesdeltaganden' key") from err
519
826
  @
520
827
 
521
828
  We add the following test.
@@ -534,6 +841,43 @@ print(r"\end{minted}")
534
841
  \end{pycode}
535
842
 
536
843
 
844
+ \section{[[studystructure_student_JSON]]}
845
+
846
+ We also want to get a student's study structure, that is programmes that they
847
+ are admitted to.
848
+ <<LadokSession data methods>>=
849
+ # added by GQMJr
850
+ def studystructure_student_JSON(self, uid):
851
+ """
852
+ Returns a dictionary of student information. This contains programmes that
853
+ the student is admitted to.
854
+ """
855
+ r = self.session.get(
856
+ url=self.base_gui_proxy_url +
857
+ '/studiedeltagande/internal/studiestruktur/student/'+uid,
858
+ headers=self.headers)
859
+ if r.status_code == 200:
860
+ return r.json()
861
+ raise LadokAPIError(f"can't get study structure for student {uid}: {r.text}")
862
+ @
863
+
864
+ Let's add a test.
865
+ This should return a dictionary.
866
+ <<test functions>>=
867
+ def test_studystructure_student_JSON():
868
+ r = ladok.studystructure_student_JSON(student_uid)
869
+ assert type(r) == dict
870
+ @
871
+
872
+ The output looks like this:
873
+ \begin{pycode}[apitest]
874
+ print(r"\begin{minted}{JSON}")
875
+ print(json.dumps(ladok3.clean_data(ladok.studystructure_student_JSON(student_uid)),
876
+ indent=2))
877
+ print(r"\end{minted}")
878
+ \end{pycode}
879
+
880
+
537
881
 
538
882
  \chapter{Course-related API calls}
539
883
 
@@ -556,11 +900,14 @@ def search_course_rounds_JSON(self, /, **kwargs):
556
900
 
557
901
  url += "page=1&limit=400&sprakkod=sv"
558
902
 
559
- response = self.get_query(url)
903
+ data = self.get_query(url)
560
904
 
561
- if response.status_code == requests.codes.ok:
562
- return response.json()["Resultat"]
563
- raise Exception(f"search_course_rounds_JSON failed: {response.text}")
905
+ try:
906
+ return data["Resultat"]
907
+ except KeyError as err:
908
+ err.add_note(f"Response data: {data}")
909
+ raise LadokAPIError(f"Unexpected response format when searching for course rounds: "
910
+ f"missing 'Resultat' key") from err
564
911
  @
565
912
 
566
913
  We add the following test.
@@ -573,9 +920,9 @@ def test_search_course_rounds_JSON():
573
920
  The output looks like this.
574
921
  \begin{pycode}[apitest]
575
922
  print(r"\begin{minted}{JSON}")
576
- prgi = ladok.search_course_rounds_JSON(code="DD1317")
577
- ladok3.clean_data(prgi)
578
- print(json.dumps(prgi, indent=2, ensure_ascii=False))
923
+ course_rounds = ladok.search_course_rounds_JSON(code="DD1317")
924
+ ladok3.clean_data(course_rounds)
925
+ print(json.dumps(course_rounds, indent=2, ensure_ascii=False))
579
926
  print(r"\end{minted}")
580
927
  \end{pycode}
581
928
 
@@ -587,13 +934,16 @@ This method fetches all course rounds that uses the given course instance.
587
934
  <<LadokSession data methods>>=
588
935
  def course_rounds_JSON(self, course_instance_id):
589
936
  """Requires course instance ID"""
590
- response = self.get_query(
591
- f"/resultat/internal/kurstillfalle/kursinstans/{course_instance_id}")
937
+ data = self.get_query(
938
+ f"/resultat/internal/kurstillfalle/kursinstans/{course_instance_id}"
939
+ )
592
940
 
593
- if response.status_code == requests.codes.ok:
594
- return response.json()["Utbildningstillfalle"]
595
- raise Exception(f"can't list course round for course {course_instance_id}: "
596
- f"{response.text}")
941
+ try:
942
+ return data["Utbildningstillfalle"]
943
+ except KeyError as err:
944
+ err.add_note(f"Response data: {data}")
945
+ raise LadokAPIError(f"Unexpected response format when fetching course rounds for "
946
+ f"instance {course_instance_id}: missing 'Utbildningstillfalle' key") from err
597
947
  @
598
948
 
599
949
  We test this method as follows.
@@ -621,14 +971,19 @@ It requires the course instance ID.
621
971
  (This is a slightly rewritten version of Maguire's original method.)
622
972
  <<LadokSession data methods>>=
623
973
  def course_instance_JSON(self, instance_id):
624
- """Returns course instance data for a course with instance ID instance_id"""
625
- response = self.get_query(
626
- f"/resultat/internal/utbildningsinstans/kursinstans/{instance_id}")
627
-
628
- if response.status_code == requests.codes.ok:
629
- return response.json()
630
- raise Exception(f"can't get course instance data for {instance_id}: "
631
- f"{response.text}")
974
+ """
975
+ Returns course instance data for a course with instance ID instance_id
976
+ """
977
+ try:
978
+ return self.get_query(
979
+ f"/resultat/internal/utbildningsinstans/kursinstans/{instance_id}"
980
+ )
981
+ except LadokAPIError as err:
982
+ raise LadokAPIError(f"Failed to get course instance data for "
983
+ f"{instance_id}: {err}") from err
984
+ except LadokServerError as err:
985
+ raise LadokServerError(f"LADOK server error when getting course instance data for "
986
+ f"{instance_id}: {err}") from err
632
987
  @
633
988
 
634
989
  We add the following test.
@@ -649,6 +1004,136 @@ print(r"\end{minted}")
649
1004
  \end{pycode}
650
1005
 
651
1006
 
1007
+ \section{[[course_instances_JSON]]}
1008
+
1009
+ We can get a list of course instances for a given course code.
1010
+ <<LadokSession data methods>>=
1011
+ # added by GQMJr
1012
+ def course_instances_JSON(self, course_code, lang = 'sv'):
1013
+ """
1014
+ Returns a list of dictionaries with course instances for a given course code.
1015
+ The course code is a string such as "DD1310". The language code is 'en' or
1016
+ 'sv'.
1017
+
1018
+ Note that there seems to be a limit of 403 for the number of pages.
1019
+ """
1020
+ r = self.session.get(
1021
+ url=self.base_gui_proxy_url + '/resultat/internal/kurstillfalle/filtrera?kurskod=' +
1022
+ course_code + '&page=1&limit=100&skipCount=false&sprakkod=' + lang, # not sure about this one /CO
1023
+ headers=self.headers)
1024
+ if r.status_code == requests.codes.ok:
1025
+ return r.json()
1026
+ raise LadokAPIError(f"failed to get course instances for {course_code}: {r.text}")
1027
+ @
1028
+
1029
+ Let's add a test.
1030
+ This should return a list of dictionaries.
1031
+ <<test functions>>=
1032
+ def test_course_instances_JSON():
1033
+ r = ladok.course_instances_JSON('DD2395')
1034
+ assert type(r) == dict
1035
+ @
1036
+
1037
+ The output looks like this:
1038
+ \begin{pycode}[apitest]
1039
+ print(r"\begin{minted}{JSON}")
1040
+ print(json.dumps(ladok3.clean_data(ladok.course_instances_JSON('DD2395')),
1041
+ indent=2))
1042
+ print(r"\end{minted}")
1043
+ \end{pycode}
1044
+
1045
+
1046
+ \section{[[instance_info]]}
1047
+
1048
+ This function gets a course instance by the combination of course code (\eg
1049
+ DD1310) and the five digit round code (\eg 50429).
1050
+ <<LadokSession data methods>>=
1051
+ # added by GQMJr
1052
+ def instance_info(self, course_code, instance_code, lang = 'sv'):
1053
+ """
1054
+ Returns a dictionary of course instance information.
1055
+
1056
+ course_code - course code, such as "DD1310"
1057
+
1058
+ instance_code - instance of the course ('TillfallesKod')
1059
+
1060
+ lang - language code 'en' or 'sv', defaults to 'sv'
1061
+ """
1062
+ r = self.session.get(
1063
+ url=self.base_gui_proxy_url +
1064
+ '/resultat/internal/kurstillfalle/filtrera?kurskod=' + course_code +
1065
+ '&page=1&limit=25&skipCount=false&sprakkod=' + lang,
1066
+ headers=self.headers)
1067
+ if r.status_code == requests.codes.ok:
1068
+ rj=r.json()
1069
+ for course in rj['Resultat']:
1070
+ if course['TillfallesKod'] == instance_code:
1071
+ return course
1072
+ # If we get here, the course instance was not found
1073
+ raise LadokNotFoundError(f"course instance {instance_code} not found for course {course_code}")
1074
+ raise LadokAPIError(f"failed to search for course instance {course_code}/{instance_code}: {r.text}")
1075
+ @
1076
+
1077
+ Let's add a test.
1078
+ This should return a dictionary.
1079
+ <<test functions>>=
1080
+ def test_instance_info():
1081
+ r = ladok.instance_info('DD1310', '50429')
1082
+ assert type(r) == dict
1083
+ @
1084
+
1085
+ The output looks like this:
1086
+ \begin{pycode}[apitest]
1087
+ print(r"\begin{minted}{JSON}")
1088
+ print(json.dumps(ladok3.clean_data(ladok.instance_info('DD1310', '50429')),
1089
+ indent=2))
1090
+ print(r"\end{minted}")
1091
+ \end{pycode}
1092
+
1093
+
1094
+ \section{[[instance_info_uid]]}
1095
+
1096
+ This function returns the same as above ([[instance_info]]) but uses the
1097
+ course's LADOK ID.
1098
+ This ID can be found as the [[sis_course_id]] in Canvas.
1099
+ <<LadokSession data methods>>=
1100
+ # added by GQMJr
1101
+ def instance_info_uid(self, instance_uid):
1102
+ """
1103
+ Returns a dictionary of course instance information.
1104
+
1105
+ instance_uid: course's Uid (from course_integration_id or
1106
+ sis_course_id in Canvas)
1107
+ """
1108
+ r = self.session.get(
1109
+ url=self.base_gui_proxy_url +
1110
+ '/resultat/internal/kurstillfalle/'+instance_uid,
1111
+ headers=self.headers)
1112
+ if r.status_code == requests.codes.ok:
1113
+ return r.json()
1114
+ raise LadokAPIError(f"failed to get course instance info for {instance_uid}: {r.text}")
1115
+ @
1116
+
1117
+ Let's add a test.
1118
+ This should return a dictionary.
1119
+ <<test functions>>=
1120
+ def test_instance_info_uid():
1121
+ r = ladok.instance_info_uid(dasak_round_id)
1122
+ assert type(r) == dict
1123
+ @
1124
+
1125
+ The output looks like this:
1126
+ \begin{pycode}[apitest]
1127
+ print(r"\begin{minted}{JSON}")
1128
+ print(json.dumps(ladok3.clean_data(
1129
+ ladok.instance_info_uid(dasak_round_id)),
1130
+ indent=2))
1131
+ print(r"\end{minted}")
1132
+ \end{pycode}
1133
+
1134
+
1135
+
1136
+
652
1137
  \section{Course components}
653
1138
 
654
1139
  There are two ways to get the components for a course.
@@ -661,14 +1146,29 @@ This one includes data such as the number of registered students as well,
661
1146
  unlike the method in the next section.
662
1147
  <<LadokSession data methods>>=
663
1148
  def course_round_components_JSON(self, round_id):
664
- response = self.put_query(
1149
+ """
1150
+ Fetch course components for a given course round.
1151
+
1152
+ Args:
1153
+ round_id (str): The unique identifier of the course round.
1154
+
1155
+ Returns:
1156
+ list: A list of course component JSON objects.
1157
+
1158
+ Raises:
1159
+ LadokServerError: If the request fails or server returns an error message.
1160
+ """
1161
+ data = self.put_query(
665
1162
  "/resultat/internal/kurstillfalle/moment",
666
1163
  {"Identitet": [round_id]}
667
1164
  )
668
1165
 
669
- if response.status_code == 200:
670
- return response.json()["MomentPerKurstillfallen"][0]["Moment"]
671
- raise Exception(response.json()["Meddelande"])
1166
+ try:
1167
+ return data["MomentPerKurstillfallen"][0]["Moment"]
1168
+ except (KeyError, IndexError) as err:
1169
+ err.add_note(f"Response data: {data}")
1170
+ raise LadokAPIError(f"Unexpected response format when fetching components for "
1171
+ f"round {round_id}: {type(err).__name__}") from err
672
1172
  @
673
1173
 
674
1174
  We add the following test.
@@ -698,14 +1198,29 @@ This method fetches the course components for a course instance, \ie a version
698
1198
  of the syllabus.
699
1199
  <<LadokSession data methods>>=
700
1200
  def course_instance_components_JSON(self, course_instance_id):
701
- response = self.put_query(
1201
+ """
1202
+ Fetch course components for a given course instance.
1203
+
1204
+ Args:
1205
+ course_instance_id (str): The unique identifier of the course instance.
1206
+
1207
+ Returns:
1208
+ dict: JSON object containing course instance components.
1209
+
1210
+ Raises:
1211
+ LadokServerError: If the request fails or server returns an error message.
1212
+ """
1213
+ data = self.put_query(
702
1214
  "/resultat/internal/utbildningsinstans/moduler",
703
1215
  {"Identitet": [course_instance_id]}
704
1216
  )
705
1217
 
706
- if response.status_code == requests.codes.ok:
707
- return response.json()["Utbildningsinstans"][0]
708
- raise Exception(response.json()["Meddelande"])
1218
+ try:
1219
+ return data["Utbildningsinstans"][0]
1220
+ except (KeyError, IndexError) as err:
1221
+ err.add_note(f"Response data: {data}")
1222
+ raise LadokAPIError(f"Unexpected response format when fetching components for "
1223
+ f"instance {course_instance_id}: {type(err).__name__}") from err
709
1224
  @
710
1225
 
711
1226
  We add the following test code.
@@ -762,16 +1277,18 @@ def search_reported_results_JSON(self, course_round_id, component_instance_id):
762
1277
  "StudenterUID": []
763
1278
  }
764
1279
 
765
- response = self.put_query(
1280
+ data = self.put_query(
766
1281
  "/resultat/internal/studieresultat/rapportera"
767
1282
  f"/utbildningsinstans/{component_instance_id}/sok",
768
1283
  put_data)
769
1284
 
770
- if response.status_code == requests.codes.ok:
771
- return response.json()["Resultat"]
772
- raise Exception(f"failed searching for results for "
773
- f"course {course_round_id}, "
774
- f"component {component_instance_id}: {response.text}")
1285
+ try:
1286
+ return data["Resultat"]
1287
+ except KeyError as err:
1288
+ err.add_note(f"Response data: {data}")
1289
+ raise LadokAPIError(f"Unexpected response format when searching for results for "
1290
+ f"round {course_round_id}, component {component_instance_id}: "
1291
+ f"missing 'Resultat' key") from err
775
1292
  @
776
1293
 
777
1294
  We write the following test.
@@ -800,6 +1317,19 @@ print(r"\end{minted}")
800
1317
  Another method, which gives slightly different results is the following.
801
1318
  <<LadokSession data methods>>=
802
1319
  def search_course_results_JSON(self, course_round_id, component_instance_id):
1320
+ """
1321
+ Retrieve course results for a specific component in a course round.
1322
+
1323
+ Args:
1324
+ course_round_id (str): The unique identifier of the course round.
1325
+ component_instance_id (str): The unique identifier of the component instance.
1326
+
1327
+ Returns:
1328
+ list: List of course result JSON objects.
1329
+
1330
+ Raises:
1331
+ LadokAPIError: If the request fails.
1332
+ """
803
1333
  put_data = {
804
1334
  "KurstillfallenUID": [course_round_id],
805
1335
  "Tillstand": ["REGISTRERAD", "AVKLARAD", "AVBROTT"],
@@ -808,15 +1338,17 @@ def search_course_results_JSON(self, course_round_id, component_instance_id):
808
1338
  "Page": 1,
809
1339
  }
810
1340
 
811
- response = self.put_query(
1341
+ data = self.put_query(
812
1342
  "/resultat/internal/resultatuppfoljning/resultatuppfoljning/sok",
813
1343
  put_data)
814
1344
 
815
- if response.status_code == requests.codes.ok:
816
- return response.json()["Resultat"]
817
- raise Exception("can't get course results for "
818
- f"course {course_round_id}, "
819
- f"component {component_instance_id}: {response.text}")
1345
+ try:
1346
+ return data["Resultat"]
1347
+ except KeyError as err:
1348
+ err.add_note(f"Response data: {data}")
1349
+ raise LadokAPIError(f"Unexpected response format when searching course results for "
1350
+ f"round {course_round_id}, component {component_instance_id}: "
1351
+ f"missing 'Resultat' key") from err
820
1352
  @
821
1353
 
822
1354
  We test this by the following.
@@ -847,14 +1379,18 @@ LADOK changed this API request in 2022.
847
1379
  <<LadokSession data methods>>=
848
1380
  def student_results_JSON(self, student_id, course_education_id):
849
1381
  """Returns the results for a student on a course"""
850
- response = self.get_query(
851
- "/resultat/internal/studentenskurser/kursinformation"
852
- f"/student/{student_id}/kursUID/{course_education_id}"
853
- )
854
-
855
- if response.status_code == requests.codes.ok:
856
- return response.json()
857
- raise Exception(response.json()["Meddelande"])
1382
+ try:
1383
+ return self.get_query(
1384
+ "/resultat/internal/studentenskurser/kursinformation"
1385
+ f"/student/{student_id}/kursUID/{course_education_id}"
1386
+ )
1387
+ except LadokAPIError as err:
1388
+ raise LadokAPIError(f"Failed to get results for student {student_id} "
1389
+ f"on course {course_education_id}: {err}") from err
1390
+ except LadokServerError as err:
1391
+ raise LadokServerError(f"LADOK server error when getting results for "
1392
+ f"student {student_id} on course {course_education_id}: "
1393
+ f"{err}") from err
858
1394
  @
859
1395
 
860
1396
  We test this in the following way.
@@ -897,23 +1433,28 @@ def create_result_JSON(self,
897
1433
  student_id, course_instance_id, component_id,
898
1434
  grade_id, date,
899
1435
  project_title=None):
900
- """Creates a new result"""
901
- response = self.post_query(
902
- f"/resultat/internal/resultat/student/{student_id}"
903
- f"/kursinstans/{course_instance_id}"
904
- f"/utbildningsinstans/{component_id}"
905
- f"/skapa",
906
- {
907
- "Betygsgrad": grade_id,
908
- "Examinationsdatum": date,
909
- "Projekttitel": project_title
910
- }
911
- )
912
-
913
- if response.status_code == requests.codes.ok:
914
- return response.json()
915
- raise Exception(f"LADOK create request failed: "
916
- f"{response.json()['Meddelande']}")
1436
+ """
1437
+ Creates a new result
1438
+ """
1439
+ try:
1440
+ return self.post_query(
1441
+ f"/resultat/internal/resultat/student/{student_id}"
1442
+ f"/kursinstans/{course_instance_id}"
1443
+ f"/utbildningsinstans/{component_id}"
1444
+ f"/skapa",
1445
+ {
1446
+ "Betygsgrad": grade_id,
1447
+ "Examinationsdatum": date,
1448
+ "Projekttitel": project_title
1449
+ }
1450
+ )
1451
+ except LadokAPIError as err:
1452
+ raise LadokAPIError(f"Failed to create result for student {student_id} "
1453
+ f"on component {component_id}: {err}") from err
1454
+ except LadokServerError as err:
1455
+ raise LadokServerError(f"LADOK server error when creating result for "
1456
+ f"student {student_id} on component {component_id}: "
1457
+ f"{err}") from err
917
1458
  @
918
1459
 
919
1460
  We test this in the following way.
@@ -965,20 +1506,38 @@ we did for [[create_result_JSON]].
965
1506
  <<LadokSession data methods>>=
966
1507
  def update_result_JSON(self,
967
1508
  result_id, grade_id, date, last_modified, notes=[]):
968
- response = self.put_query(
969
- f"/resultat/internal/resultat/uppdatera/{result_id}",
970
- {
971
- 'Betygsgrad': grade_id,
972
- 'Examinationsdatum': date,
973
- 'Noteringar': notes,
974
- 'SenasteResultatandring': last_modified
975
- }
976
- )
977
-
978
- if response.status_code == requests.codes.ok:
979
- return response.json()
980
- raise Exception(f"LADOK request to modify result failed: "
981
- f"{response.json()['Meddelande']}")
1509
+ """Update an existing result draft in LADOK.
1510
+
1511
+ Note: Cannot be used to update finalized results. Uses ResultatUID, not StudieresultatUID.
1512
+
1513
+ Args:
1514
+ result_id (str): The unique result identifier (ResultatUID).
1515
+ grade_id (int): The numeric grade identifier from the grade scale.
1516
+ date (str): Examination date in YYYY-MM-DD format.
1517
+ last_modified (str): Last modification timestamp to prevent conflicts.
1518
+ notes (list, optional): List of notes to attach to the result.
1519
+
1520
+ Returns:
1521
+ dict: Updated result data from LADOK.
1522
+
1523
+ Raises:
1524
+ Exception: If the update request fails or is rejected by LADOK.
1525
+ """
1526
+ try:
1527
+ return self.put_query(
1528
+ f"/resultat/internal/resultat/uppdatera/{result_id}",
1529
+ {
1530
+ 'Betygsgrad': grade_id,
1531
+ 'Examinationsdatum': date,
1532
+ 'Noteringar': notes,
1533
+ 'SenasteResultatandring': last_modified
1534
+ }
1535
+ )
1536
+ except LadokAPIError as err:
1537
+ raise LadokAPIError(f"Failed to update result {result_id}: {err}") from err
1538
+ except LadokServerError as err:
1539
+ raise LadokServerError(f"LADOK server error when updating result "
1540
+ f"{result_id}: {err}") from err
982
1541
  @
983
1542
 
984
1543
  We test this in the following way.
@@ -1022,14 +1581,17 @@ We start with who can attest.
1022
1581
  <<LadokSession data methods>>=
1023
1582
  def result_attestants_JSON(self, result_id):
1024
1583
  """Returns a list of result attestants"""
1025
- response = self.put_query(
1584
+ data = self.put_query(
1026
1585
  "/resultat/internal/anvandare/resultatrattighet/attestanter/kurstillfallesrapportering",
1027
1586
  {"Identitet": [result_id]}
1028
1587
  )
1029
1588
 
1030
- if response.status_code == 200:
1031
- return response.json()["Anvandare"]
1032
- raise Exception(response.json()["Meddelande"])
1589
+ try:
1590
+ return data["Anvandare"]
1591
+ except KeyError as err:
1592
+ err.add_note(f"Response data: {data}")
1593
+ raise LadokAPIError(f"Unexpected response format when fetching attestants for "
1594
+ f"result {result_id}: missing 'Anvandare' key") from err
1033
1595
  @ The [[result_id]] is the ID returned in the [[ResultatUID]] field in the
1034
1596
  response from the [[create_result_JSON]] method.
1035
1597
 
@@ -1061,15 +1623,18 @@ organization).
1061
1623
  <<LadokSession data methods>>=
1062
1624
  def result_reporters_JSON(self, organization_id):
1063
1625
  """Returns a list of who can report results in an organization"""
1064
- response = self.get_query(
1626
+ data = self.get_query(
1065
1627
  "/kataloginformation/internal/anvandare/organisation/" +
1066
1628
  organization_id + "/resultatrapportorer",
1067
1629
  "application/vnd.ladok-kataloginformation+json"
1068
1630
  )
1069
1631
 
1070
- if response.status_code == 200:
1071
- return response.json()["Anvandare"]
1072
- raise Exception(response.text)
1632
+ try:
1633
+ return data["Anvandare"]
1634
+ except KeyError as err:
1635
+ err.add_note(f"Response data: {data}")
1636
+ raise LadokAPIError(f"Unexpected response format when fetching reporters for "
1637
+ f"organization {organization_id}: missing 'Anvandare' key") from err
1073
1638
  @
1074
1639
 
1075
1640
  We test this method as follows.
@@ -1096,14 +1661,25 @@ Usually, we want to set the reporter to the logged-in user.
1096
1661
  We can use the following API call to get information about the logged-in user.
1097
1662
  <<LadokSession data methods>>=
1098
1663
  def user_info_JSON(self):
1099
- response = self.get_query(
1100
- "/kataloginformation/internal/anvandare/anvandarinformation",
1101
- "application/vnd.ladok-kataloginformation+json"
1102
- )
1103
-
1104
- if response.status_code == 200:
1105
- return response.json()
1106
- raise Exception(response.text)
1664
+ """
1665
+ Get information about the currently logged-in user.
1666
+
1667
+ Returns:
1668
+ dict: User information including name, roles, and permissions.
1669
+
1670
+ Raises:
1671
+ Exception: If the request fails or returns an error status.
1672
+ """
1673
+ try:
1674
+ return self.get_query(
1675
+ "/kataloginformation/internal/anvandare/anvandarinformation",
1676
+ "application/vnd.ladok-kataloginformation+json"
1677
+ )
1678
+ except LadokAPIError as err:
1679
+ raise LadokAPIError(f"Failed to get user info: {err}") from err
1680
+ except LadokServerError as err:
1681
+ raise LadokServerError(f"LADOK server error when getting user info: "
1682
+ f"{err}") from err
1107
1683
  @
1108
1684
 
1109
1685
  We test this as follows.
@@ -1134,20 +1710,22 @@ def finalize_result_JSON(self,
1134
1710
  result_id, last_modified, reporter_id, attestant_ids=[],
1135
1711
  others=[]):
1136
1712
  """Marks a result as finalized (klarmarkera)"""
1137
- response = self.put_query(
1138
- f"/resultat/internal/resultat/klarmarkera/{result_id}",
1139
- {
1140
- "Beslutsfattare": attestant_ids,
1141
- "KlarmarkeradAvUID": reporter_id,
1142
- "RattadAv": [],
1143
- "OvrigaMedverkande": "\n".join(set(others)),
1144
- "ResultatetsSenastSparad": last_modified
1145
- }
1146
- )
1147
-
1148
- if response.status_code == requests.codes.ok:
1149
- return response.json()
1150
- raise Exception(response.json()["Meddelande"])
1713
+ try:
1714
+ return self.put_query(
1715
+ f"/resultat/internal/resultat/klarmarkera/{result_id}",
1716
+ {
1717
+ "Beslutsfattare": attestant_ids,
1718
+ "KlarmarkeradAvUID": reporter_id,
1719
+ "RattadAv": [],
1720
+ "OvrigaMedverkande": "\n".join(set(others)),
1721
+ "ResultatetsSenastSparad": last_modified
1722
+ }
1723
+ )
1724
+ except LadokAPIError as err:
1725
+ raise LadokAPIError(f"Failed to finalize result {result_id}: {err}") from err
1726
+ except LadokServerError as err:
1727
+ raise LadokServerError(f"LADOK server error when finalizing result "
1728
+ f"{result_id}: {err}") from err
1151
1729
  @ This method returns a copy of the finalized result.
1152
1730
 
1153
1731
  We test this in the following way.
@@ -1187,20 +1765,22 @@ uses [[uppdateraklarmarkerat]] instead of just [[uppdatera]].
1187
1765
  <<LadokSession data methods>>=
1188
1766
  def update_finalized_result_JSON(self,
1189
1767
  result_id, grade_id, date, last_modified, notes=[]):
1190
- response = self.put_query(
1191
- f"/resultat/internal/resultat/uppdateraklarmarkerat/{result_id}",
1192
- {
1193
- 'Betygsgrad': grade_id,
1194
- 'Examinationsdatum': date,
1195
- 'Noteringar': notes,
1196
- 'SenasteResultatandring': last_modified
1197
- }
1198
- )
1199
-
1200
- if response.status_code == requests.codes.ok:
1201
- return response.json()
1202
- raise Exception(f"LADOK request to modify finalized result failed: "
1203
- f"{response.json()['Meddelande']}")
1768
+ try:
1769
+ return self.put_query(
1770
+ f"/resultat/internal/resultat/uppdateraklarmarkerat/{result_id}",
1771
+ {
1772
+ 'Betygsgrad': grade_id,
1773
+ 'Examinationsdatum': date,
1774
+ 'Noteringar': notes,
1775
+ 'SenasteResultatandring': last_modified
1776
+ }
1777
+ )
1778
+ except LadokAPIError as err:
1779
+ raise LadokAPIError(f"Failed to update finalized result {result_id}: "
1780
+ f"{err}") from err
1781
+ except LadokServerError as err:
1782
+ raise LadokServerError(f"LADOK server error when updating finalized result "
1783
+ f"{result_id}: {err}") from err
1204
1784
  @
1205
1785
 
1206
1786
  We test this in the following way.
@@ -1233,18 +1813,20 @@ print(r"\end{minted}")
1233
1813
  We can also change a result from finalized back to draft status.
1234
1814
  <<LadokSession data methods>>=
1235
1815
  def finalized_result_to_draft_JSON(self, result_id, last_modified):
1236
- response = self.put_query(
1237
- f"/resultat/internal/resultat/tillbakatillutkast/{result_id}",
1238
- {
1239
- 'ResultatUID': result_id,
1240
- 'ResultatetsSenastSparad': last_modified
1241
- }
1242
- )
1243
-
1244
- if response.status_code == requests.codes.ok:
1245
- return response.json()
1246
- raise Exception(f"LADOK request to change finalized result to draft failed: "
1247
- f"{response.json()['Meddelande']}")
1816
+ try:
1817
+ return self.put_query(
1818
+ f"/resultat/internal/resultat/tillbakatillutkast/{result_id}",
1819
+ {
1820
+ 'ResultatUID': result_id,
1821
+ 'ResultatetsSenastSparad': last_modified
1822
+ }
1823
+ )
1824
+ except LadokAPIError as err:
1825
+ raise LadokAPIError(f"Failed to change finalized result {result_id} to draft: "
1826
+ f"{err}") from err
1827
+ except LadokServerError as err:
1828
+ raise LadokServerError(f"LADOK server error when changing finalized result "
1829
+ f"{result_id} to draft: {err}") from err
1248
1830
  @
1249
1831
 
1250
1832
  We test this in the following way.
@@ -1275,13 +1857,17 @@ print(r"\end{minted}")
1275
1857
  We can also change a result from finalized back to draft status.
1276
1858
  <<LadokSession data methods>>=
1277
1859
  def remove_result_draft_JSON(self, result_id):
1278
- response = self.del_query(
1279
- f"/resultat/internal/resultat/tabort/{result_id}"
1280
- )
1281
-
1282
- if response.status_code != 204:
1283
- raise Exception(f"LADOK request to remove draft result failed: "
1284
- f"{response.status_code}: {response.text}")
1860
+ try:
1861
+ data = self.del_query(
1862
+ f"/resultat/internal/resultat/tabort/{result_id}"
1863
+ )
1864
+ except LadokAPIError as err:
1865
+ raise LadokAPIError(f"LADOK request to remove draft result "
1866
+ f"{result_id} failed: {err}") from err
1867
+ except LadokServerError as err:
1868
+ raise LadokServerError(f"LADOK server error when removing "
1869
+ f"draft result {result_id}: "
1870
+ f"{err}") from err
1285
1871
  @
1286
1872
 
1287
1873
  We test this in the following way.
@@ -1344,11 +1930,12 @@ def participants_JSON(self, course_round_id, /, **kwargs):
1344
1930
  '/studiedeltagande/internal/deltagare/kurstillfalle',
1345
1931
  put_data,
1346
1932
  "application/vnd.ladok-studiedeltagande+json")
1347
- if response.status_code == requests.codes.ok:
1348
- return response.json()["Resultat"]
1349
- raise Exception(f"can't get participants "
1350
- f"with course_round_id = {course_round_id}: "
1351
- f"{response.text}")
1933
+ try:
1934
+ return response["Resultat"]
1935
+ except KeyError as err:
1936
+ err.add_note(f"Response data: {response}")
1937
+ raise LadokAPIError(f"Unexpected response format when fetching participants for "
1938
+ f"round {course_round_id}: missing 'Resultat' key") from err
1352
1939
  @
1353
1940
 
1354
1941
  We test this as follows.
@@ -1371,3 +1958,841 @@ print(json.dumps(results, indent=2, ensure_ascii=False))
1371
1958
  print(r"\end{minted}")
1372
1959
  \end{pycode}
1373
1960
 
1961
+
1962
+ \chapter{Other API calls}
1963
+
1964
+ The following API calls were explored and written by Chip Maguire.
1965
+ Bosk has merely added tests and generated example output, as well as minor
1966
+ reformatting of the code.
1967
+
1968
+ \section{[[grading_rights]]}
1969
+
1970
+ We can get a list of our rights in LADOK.
1971
+ <<LadokSession data methods>>=
1972
+ # added by GQMJr
1973
+ def grading_rights(self):
1974
+ """
1975
+ Returns a list of dictionaries with the grading rights of the logged in user.
1976
+ """
1977
+ r = self.session.get(
1978
+ url=self.base_gui_proxy_url +
1979
+ '/resultat/internal/resultatrattighet/listaforinloggadanvandare',
1980
+ headers=self.headers)
1981
+ if r.status_code == requests.codes.ok:
1982
+ return r.json()['Resultatrattighet']
1983
+ raise LadokAPIError(f"failed to get grading rights: {r.text}")
1984
+ @
1985
+
1986
+ Let's add a test.
1987
+ This should return a list of dictionaries.
1988
+ <<test functions>>=
1989
+ def test_grading_rights():
1990
+ r = ladok.grading_rights()
1991
+ assert type(r) == list
1992
+ assert type(r[0]) == dict
1993
+ @
1994
+
1995
+ The output looks like this:
1996
+ \begin{pycode}[apitest]
1997
+ print(r"\begin{minted}{JSON}")
1998
+ print(json.dumps(ladok3.clean_data(ladok.grading_rights()), indent=2))
1999
+ print(r"\end{minted}")
2000
+ \end{pycode}
2001
+
2002
+
2003
+ \section{[[organization_info_JSON]]}
2004
+
2005
+ We can get information about the organization.
2006
+ <<LadokSession data methods>>=
2007
+ # added by GQMJr
2008
+ def organization_info_JSON(self):
2009
+ """
2010
+ Returns a dictionary of organization information for the entire institution
2011
+ of the logged in user.
2012
+ """
2013
+ r = self.session.get(
2014
+ url=self.base_gui_proxy_url + '/resultat/internal/organisation/utanlankar',
2015
+ headers=self.headers)
2016
+ if r.status_code == requests.codes.ok:
2017
+ return r.json()
2018
+ raise LadokAPIError(f"failed to get organization info: {r.text}")
2019
+ @
2020
+
2021
+ Let's add a test.
2022
+ This should return a dictionary.
2023
+ <<test functions>>=
2024
+ def test_organization_info_JSON():
2025
+ r = ladok.organization_info_JSON()
2026
+ assert type(r) == dict
2027
+ @
2028
+
2029
+ The output looks like this:
2030
+ \begin{pycode}[apitest]
2031
+ print(r"\begin{minted}{JSON}")
2032
+ print(json.dumps(ladok3.clean_data(ladok.organization_info_JSON()), indent=2))
2033
+ print(r"\end{minted}")
2034
+ \end{pycode}
2035
+
2036
+
2037
+ \section{[[larosatesinformation_JSON]]}
2038
+
2039
+ <<LadokSession data methods>>=
2040
+ # added by GQMJr
2041
+ def larosatesinformation_JSON(self):
2042
+ """
2043
+ Returns a dictionary of the university or college information.
2044
+ """
2045
+ r = self.session.get(
2046
+ url=self.base_gui_proxy_url +
2047
+ '/kataloginformation/internal/grunddata/larosatesinformation',
2048
+ headers=self.headers).json()
2049
+ return r
2050
+ @
2051
+
2052
+ Let's add a test.
2053
+ This should return a dictionary.
2054
+ <<test functions>>=
2055
+ def test_larosatesinformation_JSON():
2056
+ r = ladok.larosatesinformation_JSON()
2057
+ assert type(r) == dict
2058
+ @
2059
+
2060
+ The output looks like this:
2061
+ \begin{pycode}[apitest]
2062
+ print(r"\begin{minted}{JSON}")
2063
+ print(json.dumps(ladok3.clean_data(ladok.larosatesinformation_JSON()),
2064
+ indent=2))
2065
+ print(r"\end{minted}")
2066
+ \end{pycode}
2067
+
2068
+
2069
+ \section{[[undervisningssprak_JSON]]}
2070
+
2071
+ <<LadokSession data methods>>=
2072
+ # added by GQMJr
2073
+ def undervisningssprak_JSON(self):
2074
+ """
2075
+ Returns a dictionary of teaching languages.
2076
+ """
2077
+ r = self.session.get(
2078
+ url=self.base_gui_proxy_url +
2079
+ '/kataloginformation/internal/grunddata/undervisningssprak',
2080
+ headers=self.headers).json()
2081
+ return r
2082
+ @
2083
+
2084
+ Let's add a test.
2085
+ This should return a dictionary.
2086
+ <<test functions>>=
2087
+ def test_undervisningssprak_JSON():
2088
+ r = ladok.undervisningssprak_JSON()
2089
+ assert type(r) == dict
2090
+ @
2091
+
2092
+ The output looks like this:
2093
+ \begin{pycode}[apitest]
2094
+ print(r"\begin{minted}{JSON}")
2095
+ print(json.dumps(ladok3.clean_data(ladok.undervisningssprak_JSON()), indent=2))
2096
+ print(r"\end{minted}")
2097
+ \end{pycode}
2098
+
2099
+
2100
+ \section{[[i18n_translation_JSON]]}
2101
+
2102
+ <<LadokSession data methods>>=
2103
+ # added by GQMJr
2104
+ def i18n_translation_JSON(self, lang = 'sv'):
2105
+ """
2106
+ Returns a dictionary of i18n translations used in Ladok3.
2107
+ """
2108
+ r = self.session.get(
2109
+ url=self.base_gui_proxy_url +
2110
+ '/kataloginformation/internal/i18n/oversattningar/sprakkod/' + lang,
2111
+ headers=self.headers).json()
2112
+ return r
2113
+ @
2114
+
2115
+ Let's add a test.
2116
+ This should return a dictionary.
2117
+ <<test functions>>=
2118
+ def test_i18n_translation_JSON():
2119
+ r = ladok.i18n_translation_JSON()
2120
+ assert type(r) == dict
2121
+ @
2122
+
2123
+ The output looks like this:
2124
+ \begin{pycode}[apitest]
2125
+ print(r"\begin{minted}{JSON}")
2126
+ print(json.dumps(ladok3.clean_data(ladok.i18n_translation_JSON()), indent=2))
2127
+ print(r"\end{minted}")
2128
+ \end{pycode}
2129
+
2130
+
2131
+
2132
+ \section{[[svenskorter_JSON]]}
2133
+
2134
+ <<LadokSession data methods>>=
2135
+ # added by GQMJr
2136
+ def svenskorter_JSON(self):
2137
+ """
2138
+ Returns a dictionary of Swedish places with their KommunID.
2139
+ """
2140
+ r = self.session.get(
2141
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/svenskort',
2142
+ headers=self.headers).json()
2143
+ return r
2144
+ @
2145
+
2146
+ Let's add a test.
2147
+ This should return a dictionary.
2148
+ <<test functions>>=
2149
+ def test_svenskorter_JSON():
2150
+ r = ladok.svenskorter_JSON()
2151
+ assert type(r) == dict
2152
+ @
2153
+
2154
+ The output looks like this:
2155
+ \begin{pycode}[apitest]
2156
+ print(r"\begin{minted}{JSON}")
2157
+ print(json.dumps(ladok3.clean_data(ladok.svenskorter_JSON()), indent=2))
2158
+ print(r"\end{minted}")
2159
+ \end{pycode}
2160
+
2161
+
2162
+
2163
+ \section{[[kommuner_JSON]]}
2164
+
2165
+ <<LadokSession data methods>>=
2166
+ # added by GQMJr
2167
+ def kommuner_JSON(self):
2168
+ """
2169
+ Returns a dictionary of Swedish municipalities.
2170
+ """
2171
+ r = self.session.get(
2172
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/kommun',
2173
+ headers=self.headers).json()
2174
+ return r
2175
+ @
2176
+
2177
+ Let's add a test.
2178
+ This should return a dictionary.
2179
+ <<test functions>>=
2180
+ def test_kommuner_JSON():
2181
+ r = ladok.kommuner_JSON()
2182
+ assert type(r) == dict
2183
+ @
2184
+
2185
+ The output looks like this:
2186
+ \begin{pycode}[apitest]
2187
+ print(r"\begin{minted}{JSON}")
2188
+ print(json.dumps(ladok3.clean_data(ladok.kommuner_JSON()), indent=2))
2189
+ print(r"\end{minted}")
2190
+ \end{pycode}
2191
+
2192
+
2193
+ \section{[[lander_JSON]]}
2194
+
2195
+ <<LadokSession data methods>>=
2196
+ # added by GQMJr
2197
+ def lander_JSON(self):
2198
+ """
2199
+ Returns a dictionary of countries.
2200
+ """
2201
+ r = self.session.get(
2202
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/land',
2203
+ headers=self.headers).json()
2204
+ return r
2205
+ @
2206
+
2207
+ Let's add a test.
2208
+ This should return a dictionary.
2209
+ <<test functions>>=
2210
+ def test_lander_JSON():
2211
+ r = ladok.lander_JSON()
2212
+ assert type(r) == dict
2213
+ @
2214
+
2215
+ The output looks like this:
2216
+ \begin{pycode}[apitest]
2217
+ print(r"\begin{minted}{JSON}")
2218
+ print(json.dumps(ladok3.clean_data(ladok.lander_JSON()), indent=2))
2219
+ print(r"\end{minted}")
2220
+ \end{pycode}
2221
+
2222
+
2223
+
2224
+ \section{[[undervisningstid_JSON]]}
2225
+
2226
+ <<LadokSession data methods>>=
2227
+ # added by GQMJr
2228
+ def undervisningstid_JSON(self):
2229
+ """
2230
+ Returns a dictionary of teaching times.
2231
+ """
2232
+ r = self.session.get(
2233
+ url=self.base_gui_proxy_url +
2234
+ '/kataloginformation/internal/grunddata/undervisningstid',
2235
+ headers=self.headers).json()
2236
+ return r
2237
+ @
2238
+
2239
+ Let's add a test.
2240
+ This should return a dictionary.
2241
+ <<test functions>>=
2242
+ def test_undervisningstid_JSON():
2243
+ r = ladok.undervisningstid_JSON()
2244
+ assert type(r) == dict
2245
+ @
2246
+
2247
+ The output looks like this:
2248
+ \begin{pycode}[apitest]
2249
+ print(r"\begin{minted}{JSON}")
2250
+ print(json.dumps(ladok3.clean_data(ladok.undervisningstid_JSON()), indent=2))
2251
+ print(r"\end{minted}")
2252
+ \end{pycode}
2253
+
2254
+
2255
+
2256
+ \section{[[successivfordjupning_JSON]]}
2257
+
2258
+ <<LadokSession data methods>>=
2259
+ # RETURNERAR JSON of Successive Specializations
2260
+ def successivfordjupning_JSON(self):
2261
+ """
2262
+ Returns a dictionary of Successive Specializations.
2263
+ """
2264
+ r = self.session.get(
2265
+ url=self.base_gui_proxy_url +
2266
+ '/kataloginformation/internal/grunddata/successivfordjupning',
2267
+ headers=self.headers).json()
2268
+ return r
2269
+ @
2270
+
2271
+ Let's add a test.
2272
+ This should return a dictionary.
2273
+ <<test functions>>=
2274
+ def test_successivfordjupning_JSON():
2275
+ r = ladok.successivfordjupning_JSON()
2276
+ assert type(r) == dict
2277
+ @
2278
+
2279
+ The output looks like this:
2280
+ \begin{pycode}[apitest]
2281
+ print(r"\begin{minted}{JSON}")
2282
+ print(json.dumps(ladok3.clean_data(ladok.successivfordjupning_JSON()),
2283
+ indent=2))
2284
+ print(r"\end{minted}")
2285
+ \end{pycode}
2286
+
2287
+
2288
+ \section{[[undervisningsform_JSON]]}
2289
+
2290
+ <<LadokSession data methods>>=
2291
+ # added by GQMJr
2292
+ def undervisningsform_JSON(self):
2293
+ """
2294
+ Returns forms of education.
2295
+ """
2296
+ r = self.session.get(
2297
+ url=self.base_gui_proxy_url +
2298
+ '/kataloginformation/internal/grunddata/undervisningsform',
2299
+ headers=self.headers).json()
2300
+ return r
2301
+ @
2302
+
2303
+ Let's add a test.
2304
+ This should return a dictionary.
2305
+ <<test functions>>=
2306
+ def test_undervisningsform_JSON():
2307
+ r = ladok.undervisningsform_JSON()
2308
+ assert type(r) == dict
2309
+ @
2310
+
2311
+ The output looks like this:
2312
+ \begin{pycode}[apitest]
2313
+ print(r"\begin{minted}{JSON}")
2314
+ print(json.dumps(ladok3.clean_data(ladok.undervisningsform_JSON()), indent=2))
2315
+ print(r"\end{minted}")
2316
+ \end{pycode}
2317
+
2318
+
2319
+
2320
+ \section{[[LokalaPerioder_JSON]]}
2321
+
2322
+ <<LadokSession data methods>>=
2323
+ # added by GQMJr
2324
+ def LokalaPerioder_JSON(self):
2325
+ """
2326
+ Returns local periods.
2327
+ """
2328
+ r = self.session.get(
2329
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/period',
2330
+ headers=self.headers).json()
2331
+ return r
2332
+ @
2333
+
2334
+ Let's add a test.
2335
+ This should return a dictionary.
2336
+ <<test functions>>=
2337
+ def test_LokalaPerioder_JSON():
2338
+ r = ladok.LokalaPerioder_JSON()
2339
+ assert type(r) == dict
2340
+ @
2341
+
2342
+ The output looks like this:
2343
+ \begin{pycode}[apitest]
2344
+ print(r"\begin{minted}{JSON}")
2345
+ print(json.dumps(ladok3.clean_data(ladok.LokalaPerioder_JSON()), indent=2))
2346
+ print(r"\end{minted}")
2347
+ \end{pycode}
2348
+
2349
+
2350
+
2351
+ \section{[[nivainomstudieordning_JSON]]}
2352
+
2353
+ <<LadokSession data methods>>=
2354
+ # added by GQMJr
2355
+ def nivainomstudieordning_JSON(self):
2356
+ """
2357
+ Returns education levels.
2358
+ """
2359
+ r = self.session.get(
2360
+ url=self.base_gui_proxy_url +
2361
+ '/kataloginformation/internal/grunddata/nivainomstudieordning',
2362
+ headers=self.headers).json()
2363
+ return r
2364
+ @
2365
+
2366
+ Let's add a test.
2367
+ This should return a dictionary.
2368
+ <<test functions>>=
2369
+ def test_nivainomstudieordning_JSON():
2370
+ r = ladok.nivainomstudieordning_JSON()
2371
+ assert type(r) == dict
2372
+ @
2373
+
2374
+ The output looks like this:
2375
+ \begin{pycode}[apitest]
2376
+ print(r"\begin{minted}{JSON}")
2377
+ print(json.dumps(ladok3.clean_data(ladok.nivainomstudieordning_JSON()),
2378
+ indent=2))
2379
+ print(r"\end{minted}")
2380
+ \end{pycode}
2381
+
2382
+
2383
+
2384
+ \section{[[amnesgrupp_JSON]]}
2385
+
2386
+ <<LadokSession data methods>>=
2387
+ # added by GQMJr
2388
+ def amnesgrupp_JSON(self):
2389
+ """
2390
+ Returns subject area groups.
2391
+ """
2392
+ r = self.session.get(
2393
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/amnesgrupp',
2394
+ headers=self.headers).json()
2395
+ return r
2396
+ @
2397
+
2398
+ Let's add a test.
2399
+ This should return a dictionary.
2400
+ <<test functions>>=
2401
+ def test_amnesgrupp_JSON():
2402
+ r = ladok.amnesgrupp_JSON()
2403
+ assert type(r) == dict
2404
+ @
2405
+
2406
+ The output looks like this:
2407
+ \begin{pycode}[apitest]
2408
+ print(r"\begin{minted}{JSON}")
2409
+ print(json.dumps(ladok3.clean_data(ladok.amnesgrupp_JSON()), indent=2))
2410
+ print(r"\end{minted}")
2411
+ \end{pycode}
2412
+
2413
+
2414
+
2415
+
2416
+ \section{[[studietakt_JSON]]}
2417
+
2418
+ <<LadokSession data methods>>=
2419
+ # added by GQMJr
2420
+ def studietakt_JSON(self):
2421
+ """
2422
+ Returns study paces.
2423
+ """
2424
+ r = self.session.get(
2425
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/studietakt',
2426
+ headers=self.headers).json()
2427
+ return r
2428
+ @
2429
+
2430
+ Let's add a test.
2431
+ This should return a dictionary.
2432
+ <<test functions>>=
2433
+ def test_studietakt_JSON():
2434
+ r = ladok.studietakt_JSON()
2435
+ assert type(r) == dict
2436
+ @
2437
+
2438
+ The output looks like this:
2439
+ \begin{pycode}[apitest]
2440
+ print(r"\begin{minted}{JSON}")
2441
+ print(json.dumps(ladok3.clean_data(ladok.studietakt_JSON()), indent=2))
2442
+ print(r"\end{minted}")
2443
+ \end{pycode}
2444
+
2445
+
2446
+
2447
+
2448
+ \section{[[finansieringsform_JSON]]}
2449
+
2450
+ <<LadokSession data methods>>=
2451
+ # added by GQMJr
2452
+ def finansieringsform_JSON(self):
2453
+ """
2454
+ Returns forms of financing.
2455
+ """
2456
+ r = self.session.get(
2457
+ url=self.base_gui_proxy_url +
2458
+ '/kataloginformation/internal/grunddata/finansieringsform',
2459
+ headers=self.headers).json()
2460
+ return r
2461
+ @
2462
+
2463
+ Let's add a test.
2464
+ This should return a dictionary.
2465
+ <<test functions>>=
2466
+ def test_finansieringsform_JSON():
2467
+ r = ladok.finansieringsform_JSON()
2468
+ assert type(r) == dict
2469
+ @
2470
+
2471
+ The output looks like this:
2472
+ \begin{pycode}[apitest]
2473
+ print(r"\begin{minted}{JSON}")
2474
+ print(json.dumps(ladok3.clean_data(ladok.finansieringsform_JSON()), indent=2))
2475
+ print(r"\end{minted}")
2476
+ \end{pycode}
2477
+
2478
+
2479
+ \section{[[utbildningsomrade_JSON]]}
2480
+
2481
+ <<LadokSession data methods>>=
2482
+ # added by GQMJr
2483
+ def utbildningsomrade_JSON(self):
2484
+ """
2485
+ Returns subject areas.
2486
+ """
2487
+ r = self.session.get(
2488
+ url=self.base_gui_proxy_url +
2489
+ '/kataloginformation/internal/grunddata/utbildningsomrade',
2490
+ headers=self.headers).json()
2491
+ return r
2492
+ @
2493
+
2494
+ Let's add a test.
2495
+ This should return a dictionary.
2496
+ <<test functions>>=
2497
+ def test_utbildningsomrade_JSON():
2498
+ r = ladok.utbildningsomrade_JSON()
2499
+ assert type(r) == dict
2500
+ @
2501
+
2502
+ The output looks like this:
2503
+ \begin{pycode}[apitest]
2504
+ print(r"\begin{minted}{JSON}")
2505
+ print(json.dumps(ladok3.clean_data(ladok.utbildningsomrade_JSON()), indent=2))
2506
+ print(r"\end{minted}")
2507
+ \end{pycode}
2508
+
2509
+
2510
+ \section{[[kravpatidigarestudier_JSON]]}
2511
+
2512
+ <<LadokSession data methods>>=
2513
+ # added by GQMJr
2514
+ def kravpatidigarestudier_JSON(self):
2515
+ """
2516
+ Returns requirements for earlier studies.
2517
+ """
2518
+ r = self.session.get(
2519
+ url=self.base_gui_proxy_url +
2520
+ '/kataloginformation/internal/grunddata/kravpatidigarestudier',
2521
+ headers=self.headers).json()
2522
+ return r
2523
+ @
2524
+
2525
+ Let's add a test.
2526
+ This should return a dictionary.
2527
+ <<test functions>>=
2528
+ def test_kravpatidigarestudier_JSON():
2529
+ r = ladok.kravpatidigarestudier_JSON()
2530
+ assert type(r) == dict
2531
+ @
2532
+
2533
+ The output looks like this:
2534
+ \begin{pycode}[apitest]
2535
+ print(r"\begin{minted}{JSON}")
2536
+ print(json.dumps(ladok3.clean_data(ladok.kravpatidigarestudier_JSON()),
2537
+ indent=2))
2538
+ print(r"\end{minted}")
2539
+ \end{pycode}
2540
+
2541
+
2542
+
2543
+ \section{[[studieordning_JSON]]}
2544
+
2545
+ <<LadokSession data methods>>=
2546
+ # added by GQMJr
2547
+ def studieordning_JSON(self):
2548
+ """
2549
+ Returns study regulations.
2550
+ """
2551
+ r = self.session.get(
2552
+ url=self.base_gui_proxy_url +
2553
+ '/kataloginformation/internal/grunddata/studieordning',
2554
+ headers=self.headers).json()
2555
+ return r
2556
+ @
2557
+
2558
+ Let's add a test.
2559
+ This should return a dictionary.
2560
+ <<test functions>>=
2561
+ def test_studieordning_JSON():
2562
+ r = ladok.studieordning_JSON()
2563
+ assert type(r) == dict
2564
+ @
2565
+
2566
+ The output looks like this:
2567
+ \begin{pycode}[apitest]
2568
+ print(r"\begin{minted}{JSON}")
2569
+ print(json.dumps(ladok3.clean_data(ladok.studieordning_JSON()), indent=2))
2570
+ print(r"\end{minted}")
2571
+ \end{pycode}
2572
+
2573
+
2574
+
2575
+ \section{[[enhet_JSON]]}
2576
+
2577
+ <<LadokSession data methods>>=
2578
+ # added by GQMJr
2579
+ def enhet_JSON(self):
2580
+ """
2581
+ Returns credit units.
2582
+ """
2583
+ r = self.session.get(
2584
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/grunddata/enhet',
2585
+ headers=self.headers).json()
2586
+ return r
2587
+ @
2588
+
2589
+ Let's add a test.
2590
+ This should return a dictionary.
2591
+ <<test functions>>=
2592
+ def test_enhet_JSON():
2593
+ r = ladok.enhet_JSON()
2594
+ assert type(r) == dict
2595
+ @
2596
+
2597
+ The output looks like this:
2598
+ \begin{pycode}[apitest]
2599
+ print(r"\begin{minted}{JSON}")
2600
+ print(json.dumps(ladok3.clean_data(ladok.enhet_JSON()), indent=2))
2601
+ print(r"\end{minted}")
2602
+ \end{pycode}
2603
+
2604
+
2605
+ \section{[[studielokalisering_JSON]]}
2606
+
2607
+ <<LadokSession data methods>>=
2608
+ # added by GQMJr
2609
+ def studielokalisering_JSON(self):
2610
+ """
2611
+ Returns study locations.
2612
+ """
2613
+ r = self.session.get(
2614
+ url=self.base_gui_proxy_url +
2615
+ '/kataloginformation/internal/grunddata/studielokalisering',
2616
+ headers=self.headers).json()
2617
+ return r
2618
+ @
2619
+
2620
+ Let's add a test.
2621
+ This should return a dictionary.
2622
+ <<test functions>>=
2623
+ def test_studielokalisering_JSON():
2624
+ r = ladok.studielokalisering_JSON()
2625
+ assert type(r) == dict
2626
+ @
2627
+
2628
+ The output looks like this:
2629
+ \begin{pycode}[apitest]
2630
+ print(r"\begin{minted}{JSON}")
2631
+ print(json.dumps(ladok3.clean_data(ladok.studielokalisering_JSON()), indent=2))
2632
+ print(r"\end{minted}")
2633
+ \end{pycode}
2634
+
2635
+
2636
+ \section{[[antagningsomgang_JSON]]}
2637
+
2638
+ <<LadokSession data methods>>=
2639
+ # added by GQMJr
2640
+ def antagningsomgang_JSON(self):
2641
+ """
2642
+ Returns the admission round.
2643
+ """
2644
+ r = self.session.get(
2645
+ url=self.base_gui_proxy_url +
2646
+ '/kataloginformation/internal/grunddata/antagningsomgang',
2647
+ headers=self.headers).json()
2648
+ return r
2649
+ @
2650
+
2651
+ Let's add a test.
2652
+ This should return a dictionary.
2653
+ <<test functions>>=
2654
+ def test_antagningsomgang_JSON():
2655
+ r = ladok.antagningsomgang_JSON()
2656
+ assert type(r) == dict
2657
+ @
2658
+
2659
+ The output looks like this:
2660
+ \begin{pycode}[apitest]
2661
+ print(r"\begin{minted}{JSON}")
2662
+ print(json.dumps(ladok3.clean_data(ladok.antagningsomgang_JSON()), indent=2))
2663
+ print(r"\end{minted}")
2664
+ \end{pycode}
2665
+
2666
+
2667
+ \section{[[utbildningstyp_JSON]]}
2668
+
2669
+ <<LadokSession data methods>>=
2670
+ # added by GQMJr
2671
+ def utbildningstyp_JSON(self):
2672
+ """
2673
+ Returns types of education.
2674
+
2675
+ For information about these, see
2676
+
2677
+ https://ladok.se/wp-content/uploads/2018/01/Funktionsbeskrivning_095.pdf
2678
+ """
2679
+ r = self.session.get(
2680
+ url=self.base_gui_proxy_url +
2681
+ '/kataloginformation/internal/grunddata/utbildningstyp',
2682
+ headers=self.headers).json()
2683
+ return r
2684
+ @
2685
+
2686
+ Let's add a test.
2687
+ This should return a dictionary.
2688
+ <<test functions>>=
2689
+ def test_utbildningstyp_JSON():
2690
+ r = ladok.utbildningstyp_JSON()
2691
+ assert type(r) == dict
2692
+ @
2693
+
2694
+ The output looks like this:
2695
+ \begin{pycode}[apitest]
2696
+ print(r"\begin{minted}{JSON}")
2697
+ print(json.dumps(ladok3.clean_data(ladok.utbildningstyp_JSON()), indent=2))
2698
+ print(r"\end{minted}")
2699
+ \end{pycode}
2700
+
2701
+
2702
+ \section{[[aktivitetstillfallestyp_JSON]]}
2703
+
2704
+ <<LadokSession data methods>>=
2705
+ # added by GQMJr
2706
+ def aktivitetstillfallestyp_JSON(self):
2707
+ """
2708
+ Returns the activity types.
2709
+ """
2710
+ r = self.session.get(
2711
+ url=self.base_gui_proxy_url +
2712
+ '/kataloginformation/internal/grunddata/aktivitetstillfallestyp',
2713
+ headers=self.headers).json()
2714
+ return r
2715
+ @
2716
+
2717
+ Let's add a test.
2718
+ This should return a dictionary.
2719
+ <<test functions>>=
2720
+ def test_aktivitetstillfallestyp_JSON():
2721
+ r = ladok.aktivitetstillfallestyp_JSON()
2722
+ assert type(r) == dict
2723
+ @
2724
+
2725
+ The output looks like this:
2726
+ \begin{pycode}[apitest]
2727
+ print(r"\begin{minted}{JSON}")
2728
+ print(json.dumps(ladok3.clean_data(ladok.aktivitetstillfallestyp_JSON()),
2729
+ indent=2))
2730
+ print(r"\end{minted}")
2731
+ \end{pycode}
2732
+
2733
+
2734
+ \section{[[catalog_service_index_JSON]]}
2735
+
2736
+ <<LadokSession data methods>>=
2737
+ # added by GQMJr
2738
+ def catalog_service_index_JSON(self):
2739
+ """
2740
+ Returns the catalog service index.
2741
+ """
2742
+ r = self.session.get(
2743
+ url=self.base_gui_proxy_url + '/kataloginformation/internal/service/index',
2744
+ headers=self.headers).json()
2745
+ return r
2746
+ @
2747
+
2748
+ Let's add a test.
2749
+ This should return a dictionary.
2750
+ <<test functions>>=
2751
+ def test_catalog_service_index_JSON():
2752
+ r = ladok.catalog_service_index_JSON()
2753
+ assert type(r) == dict
2754
+ @
2755
+
2756
+ The output looks like this:
2757
+ \begin{pycode}[apitest]
2758
+ print(r"\begin{minted}{JSON}")
2759
+ print(json.dumps(ladok3.clean_data(ladok.catalog_service_index_JSON()),
2760
+ indent=2))
2761
+ print(r"\end{minted}")
2762
+ \end{pycode}
2763
+
2764
+
2765
+ \section{[[omradesbehorighet_JSON]]}
2766
+
2767
+ <<LadokSession data methods>>=
2768
+ # added by GQMJr
2769
+ def omradesbehorighet_JSON(self):
2770
+ """
2771
+ Returns områdesbehörighet. See
2772
+
2773
+ https://antagning.se/globalassets/omradesbehorigheter-hogskolan.pdf
2774
+
2775
+ for more information.
2776
+ """
2777
+ r = self.session.get(
2778
+ url=self.base_gui_proxy_url +
2779
+ '/kataloginformation/internal/grunddata/omradesbehorighet',
2780
+ headers=self.headers).json()
2781
+ return r
2782
+ @
2783
+
2784
+ Let's add a test.
2785
+ This should return a dictionary.
2786
+ <<test functions>>=
2787
+ def test_omradesbehorighet_JSON():
2788
+ r = ladok.omradesbehorighet_JSON()
2789
+ assert type(r) == dict
2790
+ @
2791
+
2792
+ The output looks like this:
2793
+ \begin{pycode}[apitest]
2794
+ print(r"\begin{minted}{JSON}")
2795
+ print(json.dumps(ladok3.clean_data(ladok.omradesbehorighet_JSON()), indent=2))
2796
+ print(r"\end{minted}")
2797
+ \end{pycode}
2798
+