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.
- doc/ltxobj/ladok3.pdf +0 -0
- ladok3/Makefile +6 -0
- ladok3/__init__.py +1443 -3350
- ladok3/api.nw +1649 -224
- ladok3/cli.nw +102 -53
- ladok3/cli.py +84 -35
- ladok3/data.nw +92 -15
- ladok3/data.py +79 -3
- ladok3/ladok.bash +35 -17
- ladok3/ladok3.nw +242 -9
- ladok3/report.nw +183 -117
- ladok3/report.py +135 -63
- ladok3/scripts.nw +244 -0
- ladok3/student.nw +69 -4
- ladok3/student.py +98 -42
- ladok3/undoc.nw +62 -3119
- {ladok3-4.13.dist-info → ladok3-5.4.dist-info}/LICENSE +1 -1
- {ladok3-4.13.dist-info → ladok3-5.4.dist-info}/METADATA +39 -17
- ladok3-5.4.dist-info/RECORD +21 -0
- {ladok3-4.13.dist-info → ladok3-5.4.dist-info}/WHEEL +1 -1
- ladok3/.gitignore +0 -10
- ladok3-4.13.dist-info/RECORD +0 -21
- {ladok3-4.13.dist-info → ladok3-5.4.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
-
|
|
338
|
+
a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
|
|
339
|
+
Technology;
|
|
308
340
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
366
|
+
a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
|
|
367
|
+
Technology;
|
|
328
368
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
29
|
+
\item either absolute date or normalized to the course start and finish.
|
|
30
30
|
\end{itemize}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
\section{The [[
|
|
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("
|
|
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
|
-
|
|
80
|
-
|
|
93
|
+
ladok.search_course_rounds(code=args.course_code),
|
|
94
|
+
args.rounds)
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
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(),
|
|
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,
|
|
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(),
|
|
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
|
-
|
|
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
|
|
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>>
|