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/cli.nw CHANGED
@@ -71,10 +71,21 @@ We will use the function [[err]] for errors and [[warn]] for warnings, both
71
71
  inspired by err(3) and warn(3) in the BSD world.
72
72
  <<functions>>=
73
73
  def err(rc, msg):
74
+ """Print error message to stderr and exit with given return code.
75
+
76
+ Args:
77
+ rc (int): Return code to exit with.
78
+ msg (str): Error message to display.
79
+ """
74
80
  print(f"{sys.argv[0]}: error: {msg}", file=sys.stderr)
75
81
  sys.exit(rc)
76
82
 
77
83
  def warn(msg):
84
+ """Print warning message to stderr.
85
+
86
+ Args:
87
+ msg (str): Warning message to display.
88
+ """
78
89
  print(f"{sys.argv[0]}: {msg}", file=sys.stderr)
79
90
  @
80
91
 
@@ -188,6 +199,18 @@ But we also want to encrypt the stored object using authenticated encryption.
188
199
  That way, we know that we can trust the pickle (which is otherwise a problem).
189
200
  <<functions>>=
190
201
  def store_ladok_session(ls, credentials):
202
+ """Store a LadokSession object to disk with encryption.
203
+
204
+ Saves the session object as an encrypted pickle file in the user's cache directory.
205
+ The credentials are used to derive an encryption key for security.
206
+
207
+ Args:
208
+ ls (LadokSession): The session object to store.
209
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
210
+
211
+ Raises:
212
+ ValueError: If credentials are missing or invalid.
213
+ """
191
214
  if not os.path.isdir(dirs.user_cache_dir):
192
215
  os.makedirs(dirs.user_cache_dir)
193
216
 
@@ -200,6 +223,17 @@ def store_ladok_session(ls, credentials):
200
223
  file.write(encrypted_ls)
201
224
 
202
225
  def restore_ladok_session(credentials):
226
+ """Restore a LadokSession object from disk.
227
+
228
+ Attempts to load and decrypt a previously stored session object. Returns None
229
+ if no cached session exists or decryption fails.
230
+
231
+ Args:
232
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
233
+
234
+ Returns:
235
+ LadokSession or None: The restored session object, or None if unavailable.
236
+ """
203
237
  file_path = dirs.user_cache_dir + "/LadokSession"
204
238
 
205
239
  if os.path.isfile(file_path):
@@ -299,21 +333,17 @@ the user.
299
333
  Manages the user's LADOK login credentials. There are three ways to supply the
300
334
  login credentials, in order of priority:
301
335
 
302
- 1) Through the system keyring: Just run `ladok login` and you'll be asked to
303
- enter the credentials and they will be stored in the keyring. Note that for
304
- this to work on the WSL platform (and possibly on Windows), you need to
305
- install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
336
+ 1) Through the environment: Just set the environment variables
306
337
 
307
- 2) Through the environment: Just set the environment variables
338
+ a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
339
+ Technology;
308
340
 
309
- a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
310
- Technology;
311
- b) LADOK_VARS, a colon-separated list of environment variables, similarly to
312
- what's done in `ladok login` --- most don't need this, but can rather set
313
- LADOK_USER (the username, e.g. dbosk@ug.kth.se) and
314
- LADOK_PASS (the password) instead.
341
+ b) LADOK_VARS, a colon-separated list of environment variables, similarly to
342
+ what's done in `ladok login` --- most don't need this, but can rather set
343
+ LADOK_USER (the username, e.g. dbosk@ug.kth.se) and LADOK_PASS (the
344
+ password) instead.
315
345
 
316
- 3) Through the configuration file: Just write
346
+ 2) Through the configuration file: Just write
317
347
 
318
348
  {{
319
349
  "institution": "the name of the university"
@@ -325,6 +355,17 @@ login credentials, in order of priority:
325
355
  option). (The keys 'username' and 'password' can be renamed to correspond to
326
356
  the necessary values if the university login system uses other names.)
327
357
 
358
+ 3) Through the system keyring: Just run `ladok login` and you'll be asked to
359
+ enter the credentials and they will be stored in the keyring. Note that for
360
+ this to work on the WSL platform (and possibly on Windows), you need to
361
+ install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
362
+
363
+ The keyring is the most secure. However, sometimes one want to try different
364
+ credentials, so the environment should override the keyring. Also, on WSL the
365
+ keyring might require you to enter a password in the terminal---this is very
366
+ inconvenient in scripts. However, when logging in, we first try to store the
367
+ credentials in the keyring.
368
+
328
369
  <<add subparsers to subp>>=
329
370
  login_parser = subp.add_parser("login",
330
371
  help="Manage login credentials",
@@ -510,52 +551,17 @@ def load_credentials(filename="config.json"):
510
551
  can be passed to `LadokSession(instiution, credential dictionary)`.
511
552
  """
512
553
 
513
- <<fetch vars from keyring>>
514
- <<fetch username and password from keyring>>
515
554
  <<fetch institution from environment>>
516
555
  <<fetch username and password from environment>>
517
556
  <<fetch vars from environment>>
518
557
  <<fetch vars from config file>>
558
+ <<fetch vars from keyring>>
559
+ <<fetch username and password from keyring>>
519
560
 
520
561
  return None, None
521
562
  @
522
563
 
523
- First we try the newest format.
524
- We try to fetch the institution and vars from the keyring.
525
-
526
- Note that [[keyring]] returns [[None]] if the key doesn't exist, it doesn't
527
- raise an exception.
528
- <<fetch vars from keyring>>=
529
- try:
530
- institution = keyring.get_password("ladok3", "institution")
531
- vars_keys = keyring.get_password("ladok3", "vars")
532
-
533
- vars = {}
534
- for key in vars_keys.split(";"):
535
- value = keyring.get_password("ladok3", key)
536
- if value:
537
- vars[key] = value
538
-
539
- if institution and vars:
540
- return institution, vars
541
- except:
542
- pass
543
- @
544
-
545
- However, if that fails, we fall back on the previous format, that only
546
- supported KTH.
547
- <<fetch username and password from keyring>>=
548
- try:
549
- institution = "KTH Royal Institute of Technology"
550
- username = keyring.get_password("ladok3", "username")
551
- password = keyring.get_password("ladok3", "password")
552
- if username and password:
553
- return institution, {"username": username, "password": password}
554
- except:
555
- pass
556
- @
557
-
558
- Next in priority is to read from the environment.
564
+ First in priority is to read from the environment.
559
565
  We try to read the institution.
560
566
  If that fails, we assume we're using the old format that only supported KTH.
561
567
  <<fetch institution from environment>>=
@@ -563,7 +569,6 @@ try:
563
569
  institution = os.environ["LADOK_INST"]
564
570
  except:
565
571
  institution = "KTH Royal Institute of Technology"
566
-
567
572
  <<fetch username and password from environment>>=
568
573
  try:
569
574
  vars = {
@@ -578,6 +583,8 @@ except:
578
583
 
579
584
  If we couldn't read the old [[LADOK_USER]] and [[LADOK_PASS]], we try to read
580
585
  the [[vars]] from the environment using [[LADOK_VARS]].
586
+ Note that we need the [[institution]] to be set from [[LADOK_INST]] above for
587
+ this.
581
588
  <<fetch vars from environment>>=
582
589
  try:
583
590
  vars_keys = os.environ["LADOK_VARS"]
@@ -605,8 +612,7 @@ it doesn't exist.
605
612
  warn(f"Variable {key} not set, ignoring.")
606
613
  @
607
614
 
608
- If none of the above worked, the last resort is to try to read the
609
- configuration file.
615
+ If none of the above worked, we try the config file next.
610
616
  We pop the institution from the configuration file (a dictionary), because then
611
617
  the remaining entries will be used as [[vars]].
612
618
  <<fetch vars from config file>>=
@@ -621,6 +627,40 @@ except:
621
627
  pass
622
628
  @
623
629
 
630
+ Lastly, if nothing else worked, we try to fetch the institution and vars from
631
+ the keyring.
632
+ Note that [[keyring]] returns [[None]] if the key doesn't exist, it doesn't
633
+ raise an exception.
634
+ <<fetch vars from keyring>>=
635
+ try:
636
+ institution = keyring.get_password("ladok3", "institution")
637
+ vars_keys = keyring.get_password("ladok3", "vars")
638
+
639
+ vars = {}
640
+ for key in vars_keys.split(";"):
641
+ value = keyring.get_password("ladok3", key)
642
+ if value:
643
+ vars[key] = value
644
+
645
+ if institution and vars:
646
+ return institution, vars
647
+ except:
648
+ pass
649
+ @
650
+
651
+ However, if that fails, we fall back on the previous format, that only
652
+ supported KTH.
653
+ <<fetch username and password from keyring>>=
654
+ try:
655
+ institution = "KTH Royal Institute of Technology"
656
+ username = keyring.get_password("ladok3", "username")
657
+ password = keyring.get_password("ladok3", "password")
658
+ if username and password:
659
+ return institution, {"username": username, "password": password}
660
+ except:
661
+ pass
662
+ @
663
+
624
664
 
625
665
  \section{Managing the cache: the \texttt{cache} command and subcommands}
626
666
 
@@ -659,6 +699,15 @@ If we don't exit using [[sys.exit]], the main program will write the cache back
659
699
  again on its exit.
660
700
  <<functions>>=
661
701
  def clear_cache(ls, args):
702
+ """Clear the cached LADOK session data.
703
+
704
+ Removes the stored encrypted session file from the user's cache directory.
705
+ Silently ignores if the file doesn't exist.
706
+
707
+ Args:
708
+ ls (LadokSession): The LADOK session (unused but required by interface).
709
+ args: Command line arguments (unused).
710
+ """
662
711
  try:
663
712
  os.remove(dirs.user_cache_dir + "/LadokSession")
664
713
  except FileNotFoundError as err:
ladok3/cli.py CHANGED
@@ -28,15 +28,38 @@ dirs = appdirs.AppDirs("ladok", "dbosk@kth.se")
28
28
 
29
29
 
30
30
  def err(rc, msg):
31
+ """Print error message to stderr and exit with given return code.
32
+
33
+ Args:
34
+ rc (int): Return code to exit with.
35
+ msg (str): Error message to display.
36
+ """
31
37
  print(f"{sys.argv[0]}: error: {msg}", file=sys.stderr)
32
38
  sys.exit(rc)
33
39
 
34
40
 
35
41
  def warn(msg):
42
+ """Print warning message to stderr.
43
+
44
+ Args:
45
+ msg (str): Warning message to display.
46
+ """
36
47
  print(f"{sys.argv[0]}: {msg}", file=sys.stderr)
37
48
 
38
49
 
39
50
  def store_ladok_session(ls, credentials):
51
+ """Store a LadokSession object to disk with encryption.
52
+
53
+ Saves the session object as an encrypted pickle file in the user's cache directory.
54
+ The credentials are used to derive an encryption key for security.
55
+
56
+ Args:
57
+ ls (LadokSession): The session object to store.
58
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
59
+
60
+ Raises:
61
+ ValueError: If credentials are missing or invalid.
62
+ """
40
63
  if not os.path.isdir(dirs.user_cache_dir):
41
64
  os.makedirs(dirs.user_cache_dir)
42
65
 
@@ -74,6 +97,17 @@ def store_ladok_session(ls, credentials):
74
97
 
75
98
 
76
99
  def restore_ladok_session(credentials):
100
+ """Restore a LadokSession object from disk.
101
+
102
+ Attempts to load and decrypt a previously stored session object. Returns None
103
+ if no cached session exists or decryption fails.
104
+
105
+ Args:
106
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
107
+
108
+ Returns:
109
+ LadokSession or None: The restored session object, or None if unavailable.
110
+ """
77
111
  file_path = dirs.user_cache_dir + "/LadokSession"
78
112
 
79
113
  if os.path.isfile(file_path):
@@ -223,33 +257,10 @@ def load_credentials(filename="config.json"):
223
257
  can be passed to `LadokSession(instiution, credential dictionary)`.
224
258
  """
225
259
 
226
- try:
227
- institution = keyring.get_password("ladok3", "institution")
228
- vars_keys = keyring.get_password("ladok3", "vars")
229
-
230
- vars = {}
231
- for key in vars_keys.split(";"):
232
- value = keyring.get_password("ladok3", key)
233
- if value:
234
- vars[key] = value
235
-
236
- if institution and vars:
237
- return institution, vars
238
- except:
239
- pass
240
- try:
241
- institution = "KTH Royal Institute of Technology"
242
- username = keyring.get_password("ladok3", "username")
243
- password = keyring.get_password("ladok3", "password")
244
- if username and password:
245
- return institution, {"username": username, "password": password}
246
- except:
247
- pass
248
260
  try:
249
261
  institution = os.environ["LADOK_INST"]
250
262
  except:
251
263
  institution = "KTH Royal Institute of Technology"
252
-
253
264
  try:
254
265
  vars = {
255
266
  "username": os.environ["LADOK_USER"],
@@ -283,11 +294,42 @@ def load_credentials(filename="config.json"):
283
294
  return institution, config
284
295
  except:
285
296
  pass
297
+ try:
298
+ institution = keyring.get_password("ladok3", "institution")
299
+ vars_keys = keyring.get_password("ladok3", "vars")
300
+
301
+ vars = {}
302
+ for key in vars_keys.split(";"):
303
+ value = keyring.get_password("ladok3", key)
304
+ if value:
305
+ vars[key] = value
306
+
307
+ if institution and vars:
308
+ return institution, vars
309
+ except:
310
+ pass
311
+ try:
312
+ institution = "KTH Royal Institute of Technology"
313
+ username = keyring.get_password("ladok3", "username")
314
+ password = keyring.get_password("ladok3", "password")
315
+ if username and password:
316
+ return institution, {"username": username, "password": password}
317
+ except:
318
+ pass
286
319
 
287
320
  return None, None
288
321
 
289
322
 
290
323
  def clear_cache(ls, args):
324
+ """Clear the cached LADOK session data.
325
+
326
+ Removes the stored encrypted session file from the user's cache directory.
327
+ Silently ignores if the file doesn't exist.
328
+
329
+ Args:
330
+ ls (LadokSession): The LADOK session (unused but required by interface).
331
+ args: Command line arguments (unused).
332
+ """
291
333
  try:
292
334
  os.remove(dirs.user_cache_dir + "/LadokSession")
293
335
  except FileNotFoundError as err:
@@ -319,21 +361,17 @@ def main():
319
361
  Manages the user's LADOK login credentials. There are three ways to supply the
320
362
  login credentials, in order of priority:
321
363
 
322
- 1) Through the system keyring: Just run `ladok login` and you'll be asked to
323
- enter the credentials and they will be stored in the keyring. Note that for
324
- this to work on the WSL platform (and possibly on Windows), you need to
325
- install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
364
+ 1) Through the environment: Just set the environment variables
326
365
 
327
- 2) Through the environment: Just set the environment variables
366
+ a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
367
+ Technology;
328
368
 
329
- a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
330
- Technology;
331
- b) LADOK_VARS, a colon-separated list of environment variables, similarly to
332
- what's done in `ladok login` --- most don't need this, but can rather set
333
- LADOK_USER (the username, e.g. dbosk@ug.kth.se) and
334
- LADOK_PASS (the password) instead.
369
+ b) LADOK_VARS, a colon-separated list of environment variables, similarly to
370
+ what's done in `ladok login` --- most don't need this, but can rather set
371
+ LADOK_USER (the username, e.g. dbosk@ug.kth.se) and LADOK_PASS (the
372
+ password) instead.
335
373
 
336
- 3) Through the configuration file: Just write
374
+ 2) Through the configuration file: Just write
337
375
 
338
376
  {{
339
377
  "institution": "the name of the university"
@@ -345,6 +383,17 @@ def main():
345
383
  option). (The keys 'username' and 'password' can be renamed to correspond to
346
384
  the necessary values if the university login system uses other names.)
347
385
 
386
+ 3) Through the system keyring: Just run `ladok login` and you'll be asked to
387
+ enter the credentials and they will be stored in the keyring. Note that for
388
+ this to work on the WSL platform (and possibly on Windows), you need to
389
+ install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
390
+
391
+ The keyring is the most secure. However, sometimes one want to try different
392
+ credentials, so the environment should override the keyring. Also, on WSL the
393
+ keyring might require you to enter a password in the terminal---this is very
394
+ inconvenient in scripts. However, when logging in, we first try to store the
395
+ credentials in the keyring.
396
+
348
397
  """,
349
398
  )
350
399
 
ladok3/data.nw CHANGED
@@ -16,7 +16,7 @@ from LADOK (\cref{DataCommand}).
16
16
  This is a subcommand for the [[ladok]] command-line interface.
17
17
  It can be used like this:
18
18
  \begin{minted}{bash}
19
- ladok data DD1315 > DD1315.csv
19
+ ladok course DD1315 > DD1315.csv
20
20
  \end{minted}
21
21
  This program will produce CSV-formated data to answer the questions above.
22
22
  The data is formated like this:
@@ -26,11 +26,11 @@ The data is formated like this:
26
26
  \item component,
27
27
  \item student (pseudonym),
28
28
  \item grade,
29
- \item normalized time.
29
+ \item either absolute date or normalized to the course start and finish.
30
30
  \end{itemize}
31
31
 
32
32
 
33
- \section{The [[data]] subcommand}\label{DataCommand}
33
+ \section{The [[course]] subcommand}\label{DataCommand}
34
34
 
35
35
  This is a subcommand run as part of the [[ladok3.cli]] module.
36
36
  We provide a function [[add_command_options]] that adds the subcommand options
@@ -47,10 +47,24 @@ import sys
47
47
  <<functions>>
48
48
 
49
49
  def add_command_options(parser):
50
+ """Add the 'course' subcommand options to the argument parser.
51
+
52
+ Creates a subparser for the course data extraction command with all
53
+ necessary arguments for filtering and output formatting.
54
+
55
+ Args:
56
+ parser (ArgumentParser): The parent parser to add the subcommand to.
57
+ """
50
58
  <<add data parser to parser>>
51
59
  <<add data command arguments to data parser>>
52
60
 
53
61
  def command(ladok, args):
62
+ """Execute the course data extraction command.
63
+
64
+ Args:
65
+ ladok (LadokSession): The LADOK session for data access.
66
+ args: Parsed command line arguments containing course and filter options.
67
+ """
54
68
  <<produce data about course specified in args>>
55
69
  @
56
70
 
@@ -59,7 +73,7 @@ def command(ladok, args):
59
73
  We add a subparser.
60
74
  We set it up to use the function [[command]].
61
75
  <<add data parser to parser>>=
62
- data_parser = parser.add_parser("data",
76
+ data_parser = parser.add_parser("course",
63
77
  help="Returns course results data in CSV form",
64
78
  description="""
65
79
  Returns the results in CSV form for all first-time registered students.
@@ -76,12 +90,13 @@ This way the user can deal with how to store the data.
76
90
  <<produce data about course specified in args>>=
77
91
  data_writer = csv.writer(sys.stdout, delimiter=args.delimiter)
78
92
  course_rounds = filter_rounds(
79
- ladok.search_course_rounds(code=args.course_code),
80
- args.rounds)
93
+ ladok.search_course_rounds(code=args.course_code),
94
+ args.rounds)
81
95
 
82
- data_writer.writerow([
83
- "Course", "Round", "Component", "Student", "Grade", "Time"
84
- ])
96
+ if args.header:
97
+ data_writer.writerow([
98
+ "Course", "Round", "Component", "Student", "Grade", "Time"
99
+ ])
85
100
  for course_round in course_rounds:
86
101
  data = extract_data_for_round(ladok, course_round, args)
87
102
 
@@ -91,6 +106,7 @@ for course_round in course_rounds:
91
106
  student, grade, time]
92
107
  )
93
108
  @ We must take a course code and a delimiter as arguments.
109
+ We also want to know if we want a header or not.
94
110
  <<add data command arguments to data parser>>=
95
111
  data_parser.add_argument("course_code",
96
112
  help="The course code of the course for which to export data")
@@ -100,6 +116,9 @@ data_parser.add_argument("-d", "--delimiter",
100
116
  help="The delimiter for the CSV output; "
101
117
  "default is a tab character to be compatible with POSIX commands, "
102
118
  "use `-d,` or `-d ,` to get comma-separated values.")
119
+
120
+ data_parser.add_argument("-H", "--header", action="store_true",
121
+ help="Print a header line with the column names.")
103
122
  @
104
123
 
105
124
  We filter the rounds.
@@ -129,24 +148,40 @@ We also need the course round through the [[course_round]] object of type
129
148
  [[CourseRound]].
130
149
  <<functions>>=
131
150
  def extract_data_for_round(ladok, course_round, args):
151
+ """Extract student result data for a specific course round.
152
+
153
+ Processes a course round to extract student results data in CSV format,
154
+ filtering by students and components as specified in args.
155
+
156
+ Args:
157
+ ladok (LadokSession): The LADOK session for data access.
158
+ course_round: The course round object to extract data from.
159
+ args: Command line arguments containing filter options.
160
+
161
+ Returns:
162
+ list: List of result data dictionaries for CSV output.
163
+ """
132
164
  <<compute start and length of the course>>
133
165
  <<get the results for the course round>>
134
166
 
135
- students = filter_students(course_round.participants(), args.students)
167
+ students = filter_students(course_round.participants(),
168
+ args.students)
136
169
 
137
170
  for student in students:
138
- student_results = filter_student_results(student, results)
171
+ student_results = filter_student_results(student,
172
+ results)
139
173
 
140
174
  <<determine if student should be included>>
141
175
 
142
- components = filter_components(course_round.components(), args.components)
176
+ components = filter_components(course_round.components(),
177
+ args.components)
143
178
 
144
179
  for component in components:
145
180
  if len(student_results) < 1:
146
181
  result_data = None
147
182
  else:
148
- result_data = filter_component_result(
149
- component, student_results[0]["ResultatPaUtbildningar"])
183
+ result_data = filter_component_result(component,
184
+ student_results[0]["ResultatPaUtbildningar"])
150
185
 
151
186
  if not result_data:
152
187
  grade = "-"
@@ -154,7 +189,29 @@ def extract_data_for_round(ladok, course_round, args):
154
189
  else:
155
190
  <<extract grade and normalized date from result data>>
156
191
 
157
- yield student, component, grade, normalized_date
192
+ <<yield [[student, component, grade]] and date>>
193
+ @
194
+
195
+ We want to yield the data in CSV form, so we simply yield a tuple.
196
+ The date is either the normalized date or the date from the result data.
197
+ The student's identifier will be either the LADOK ID or the student name and
198
+ personnummer, depending on the command line arguments.
199
+ <<yield [[student, component, grade]] and date>>=
200
+ yield student.ladok_id if args.ladok_id \
201
+ else student, \
202
+ component, \
203
+ grade, \
204
+ normalized_date if args.normalize_date \
205
+ else result_data["Examinationsdatum"] if result_data \
206
+ else None
207
+ <<add data command arguments to data parser>>=
208
+ data_parser.add_argument("-l", "--ladok-id", action="store_true",
209
+ help="Use the LADOK ID for the student, "
210
+ "otherwise the student name and personnummer "
211
+ "will be used.")
212
+ data_parser.add_argument("-n", "--normalize-date", action="store_true",
213
+ help="Normalize the date to the start of the course, "
214
+ "otherwise the date is printed as is.")
158
215
  @
159
216
 
160
217
  \subsection{Get round data}
@@ -185,6 +242,15 @@ Then we must search for a student's result in the batch of results we received
185
242
  from LADOK.
186
243
  <<functions>>=
187
244
  def filter_student_results(student, results):
245
+ """Filter results for a specific student.
246
+
247
+ Args:
248
+ student: Student object with ladok_id attribute.
249
+ results (list): List of result dictionaries from LADOK.
250
+
251
+ Returns:
252
+ list: Filtered list containing only results for the specified student.
253
+ """
188
254
  return list(filter(
189
255
  lambda x: x["Student"]["Uid"] == student.ladok_id,
190
256
  results))
@@ -193,6 +259,17 @@ def filter_student_results(student, results):
193
259
  Similarly, we want to find the result for a particular component.
194
260
  <<functions>>=
195
261
  def filter_component_result(component, results):
262
+ """Find the result data for a specific course component.
263
+
264
+ Searches through results to find the entry matching the given component.
265
+
266
+ Args:
267
+ component: Course component object to search for.
268
+ results (list): List of result dictionaries to search through.
269
+
270
+ Returns:
271
+ dict or None: Result data for the component, or None if not found.
272
+ """
196
273
  for component_result in results:
197
274
  <<get the component result data>>
198
275
  <<check component code in result data>>