ladok3 4.9__py3-none-any.whl → 4.10__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
@@ -504,7 +504,11 @@ If all fail, the function will return [[None]] for both.
504
504
  (This is due to how we handle the [[login]] command.)
505
505
  <<functions>>=
506
506
  def load_credentials(filename="config.json"):
507
- """Load credentials from environment or file named filename"""
507
+ """
508
+ Loads credentials from environment or file named filename.
509
+ Returns the tuple (instituation, credential dictionary) that
510
+ can be passed to `LadokSession(instiution, credential dictionary)`.
511
+ """
508
512
 
509
513
  <<fetch vars from keyring>>
510
514
  <<fetch username and password from keyring>>
ladok3/cli.py CHANGED
@@ -201,7 +201,11 @@ Note: Your password will be visible on screen during this process.
201
201
  if new_val:
202
202
  vars[key] = new_val
203
203
  def load_credentials(filename="config.json"):
204
- """Load credentials from environment or file named filename"""
204
+ """
205
+ Loads credentials from environment or file named filename.
206
+ Returns the tuple (instituation, credential dictionary) that
207
+ can be passed to `LadokSession(instiution, credential dictionary)`.
208
+ """
205
209
 
206
210
  try:
207
211
  institution = keyring.get_password("ladok3", "institution")
ladok3/data.py CHANGED
@@ -4,164 +4,202 @@ import ladok3
4
4
  import os
5
5
  import sys
6
6
 
7
+
7
8
  def filter_rounds(all_rounds, desired_rounds):
8
- """Returns only the round objects with round code in desired_rounds."""
9
- if not desired_rounds:
10
- return all_rounds
11
- return filter(
12
- lambda x: x.round_code in desired_rounds,
13
- all_rounds
14
- )
9
+ """Returns only the round objects with round code in desired_rounds."""
10
+ if not desired_rounds:
11
+ return all_rounds
12
+ return filter(lambda x: x.round_code in desired_rounds, all_rounds)
13
+
14
+
15
15
  def extract_data_for_round(ladok, course_round, args):
16
- course_start = course_round.start
17
- course_length = course_round.end - course_start
18
- component = course_round.components()[0]
19
- results = ladok.search_reported_results_JSON(
20
- course_round.round_id, component.instance_id)
21
-
22
- students = filter_students(course_round.participants(), args.students)
23
-
24
- for student in students:
25
- student_results = filter_student_results(student, results)
26
-
27
- if not should_include(ladok, student, course_round, student_results):
28
- continue
29
-
30
- components = filter_components(course_round.components(), args.components)
31
-
32
- for component in components:
33
- if len(student_results) < 1:
34
- result_data = None
35
- else:
36
- result_data = filter_component_result(
37
- component, student_results[0]["ResultatPaUtbildningar"])
38
-
39
- if not result_data:
40
- grade = "-"
41
- normalized_date = None
42
- else:
43
- if "Betygsgradsobjekt" in result_data:
44
- grade = result_data["Betygsgradsobjekt"]["Kod"]
45
- try:
46
- date = datetime.date.fromisoformat(
47
- result_data["Examinationsdatum"])
48
- except KeyError:
49
- normalized_date = None
50
- grade = "-"
51
- else:
52
- normalized_date = (date - course_start) / course_length
53
- if args.time_limit and normalized_date > args.time_limit:
54
- grade = "-"
55
- normalized_date = None
56
- else:
57
- grade = "-"
58
- normalized_date = None
16
+ course_start = course_round.start
17
+ course_length = course_round.end - course_start
18
+ component = course_round.components()[0]
19
+ results = ladok.search_reported_results_JSON(
20
+ course_round.round_id, component.instance_id
21
+ )
22
+
23
+ students = filter_students(course_round.participants(), args.students)
24
+
25
+ for student in students:
26
+ student_results = filter_student_results(student, results)
27
+
28
+ if not should_include(ladok, student, course_round, student_results):
29
+ continue
30
+
31
+ components = filter_components(course_round.components(), args.components)
32
+
33
+ for component in components:
34
+ if len(student_results) < 1:
35
+ result_data = None
36
+ else:
37
+ result_data = filter_component_result(
38
+ component, student_results[0]["ResultatPaUtbildningar"]
39
+ )
40
+
41
+ if not result_data:
42
+ grade = "-"
43
+ normalized_date = None
44
+ else:
45
+ if "Betygsgradsobjekt" in result_data:
46
+ grade = result_data["Betygsgradsobjekt"]["Kod"]
47
+ try:
48
+ date = datetime.date.fromisoformat(
49
+ result_data["Examinationsdatum"]
50
+ )
51
+ except KeyError:
52
+ normalized_date = None
53
+ grade = "-"
54
+ else:
55
+ normalized_date = (date - course_start) / course_length
56
+ if args.time_limit and normalized_date > args.time_limit:
57
+ grade = "-"
58
+ normalized_date = None
59
+ else:
60
+ grade = "-"
61
+ normalized_date = None
62
+
63
+ yield student, component, grade, normalized_date
64
+
59
65
 
60
- yield student, component, grade, normalized_date
61
66
  def filter_student_results(student, results):
62
- return list(filter(
63
- lambda x: x["Student"]["Uid"] == student.ladok_id,
64
- results))
67
+ return list(filter(lambda x: x["Student"]["Uid"] == student.ladok_id, results))
68
+
69
+
65
70
  def filter_component_result(component, results):
66
- for component_result in results:
67
- if "Arbetsunderlag" in component_result:
68
- result_data = component_result["Arbetsunderlag"]
69
- elif "SenastAttesteradeResultat" in component_result:
70
- result_data = component_result["SenastAttesteradeResultat"]
71
- else:
72
- continue
73
- if component.instance_id != result_data["UtbildningsinstansUID"]:
74
- continue
75
- return result_data
76
-
77
- return None
71
+ for component_result in results:
72
+ if "Arbetsunderlag" in component_result:
73
+ result_data = component_result["Arbetsunderlag"]
74
+ elif "SenastAttesteradeResultat" in component_result:
75
+ result_data = component_result["SenastAttesteradeResultat"]
76
+ else:
77
+ continue
78
+ if component.instance_id != result_data["UtbildningsinstansUID"]:
79
+ continue
80
+ return result_data
81
+
82
+ return None
83
+
84
+
78
85
  def filter_students(all_students, desired_students):
79
- """Returns only the students with personnummer in desired_students."""
80
- if not desired_students:
81
- return all_students
82
- return filter(
83
- lambda x: x.personnummer in desired_students,
84
- all_students
85
- )
86
+ """Returns only the students with personnummer in desired_students."""
87
+ if not desired_students:
88
+ return all_students
89
+ return filter(lambda x: x.personnummer in desired_students, all_students)
90
+
86
91
 
87
92
  def filter_components(all_components, desired_components):
88
- """Returns only the components with a code in the desired_components."""
89
- if not desired_components:
90
- return all_components
91
- return filter(
92
- lambda x: x.code in desired_components,
93
- all_components
94
- )
93
+ """Returns only the components with a code in the desired_components."""
94
+ if not desired_components:
95
+ return all_components
96
+ return filter(lambda x: x.code in desired_components, all_components)
97
+
98
+
95
99
  def should_include(ladok, student, course_round, result):
96
- """Returns True if student should be included, False if to be excluded"""
97
- if is_reregistered(ladok, student.ladok_id, course_round):
98
- return False
100
+ """Returns True if student should be included, False if to be excluded"""
101
+ if is_reregistered(ladok, student.ladok_id, course_round):
102
+ return False
103
+
104
+ if has_credit_transfer(result):
105
+ return False
106
+
107
+ return True
99
108
 
100
- if has_credit_transfer(result):
101
- return False
102
109
 
103
- return True
104
110
  def is_reregistered(ladok, student_id, course):
105
- """Check if the student is reregistered on the course round course."""
106
- registrations = ladok.registrations_on_course_JSON(
107
- course.education_id, student_id)
108
- registrations.sort(
109
- key=lambda x: x["Utbildningsinformation"]["Studieperiod"]["Startdatum"])
110
- first_reg = registrations[0]
111
- return first_reg["Utbildningsinformation"]["Utbildningstillfalleskod"] != \
112
- course.round_code
111
+ """Check if the student is reregistered on the course round course."""
112
+ registrations = ladok.registrations_on_course_JSON(course.education_id, student_id)
113
+ registrations.sort(
114
+ key=lambda x: x["Utbildningsinformation"]["Studieperiod"]["Startdatum"]
115
+ )
116
+ first_reg = registrations[0]
117
+ return (
118
+ first_reg["Utbildningsinformation"]["Utbildningstillfalleskod"]
119
+ != course.round_code
120
+ )
121
+
122
+
113
123
  def has_credit_transfer(results):
114
- """Returns True if there exists a credit tranfer among the results."""
115
- for result in results:
116
- for component_result in result["ResultatPaUtbildningar"]:
117
- if component_result["HarTillgodoraknande"]:
118
- return True
124
+ """Returns True if there exists a credit tranfer among the results."""
125
+ for result in results:
126
+ for component_result in result["ResultatPaUtbildningar"]:
127
+ if component_result["HarTillgodoraknande"]:
128
+ return True
129
+
130
+ return False
119
131
 
120
- return False
121
132
 
122
133
  def add_command_options(parser):
123
- data_parser = parser.add_parser("data",
124
- help="Returns course results data in CSV form",
125
- description="""
134
+ data_parser = parser.add_parser(
135
+ "data",
136
+ help="Returns course results data in CSV form",
137
+ description="""
126
138
  Returns the results in CSV form for all first-time registered students.
127
- """.strip())
128
- data_parser.set_defaults(func=command)
129
- data_parser.add_argument("course_code",
130
- help="The course code of the course for which to export data")
131
-
132
- data_parser.add_argument("-d", "--delimiter",
133
- default="\t",
134
- help="The delimiter for the CSV output; "
135
- "default is a tab character to be compatible with POSIX commands, "
136
- "use `-d,` or `-d ,` to get comma-separated values.")
137
- data_parser.add_argument("-r", "--rounds", nargs="+",
138
- help="The round codes for the rounds to include, "
139
- "otherwise all rounds will be included.")
140
- data_parser.add_argument("-t", "--time-limit", type=float,
141
- help="The time (normalized) for cutting off results, "
142
- "use `-t 1.0` to cut off at course end.")
143
- data_parser.add_argument("-s", "--students", nargs="+",
144
- help="List of personnummer for students to include, "
145
- "otherwise all students will be included.")
146
-
147
- data_parser.add_argument("-c", "--components", nargs="+",
148
- help="List of component codes for components to include, "
149
- "otherwise all components will be included.")
139
+ """.strip(),
140
+ )
141
+ data_parser.set_defaults(func=command)
142
+ data_parser.add_argument(
143
+ "course_code", help="The course code of the course for which to export data"
144
+ )
145
+
146
+ data_parser.add_argument(
147
+ "-d",
148
+ "--delimiter",
149
+ default="\t",
150
+ help="The delimiter for the CSV output; "
151
+ "default is a tab character to be compatible with POSIX commands, "
152
+ "use `-d,` or `-d ,` to get comma-separated values.",
153
+ )
154
+ data_parser.add_argument(
155
+ "-r",
156
+ "--rounds",
157
+ nargs="+",
158
+ help="The round codes for the rounds to include, "
159
+ "otherwise all rounds will be included.",
160
+ )
161
+ data_parser.add_argument(
162
+ "-t",
163
+ "--time-limit",
164
+ type=float,
165
+ help="The time (normalized) for cutting off results, "
166
+ "use `-t 1.0` to cut off at course end.",
167
+ )
168
+ data_parser.add_argument(
169
+ "-s",
170
+ "--students",
171
+ nargs="+",
172
+ help="List of personnummer for students to include, "
173
+ "otherwise all students will be included.",
174
+ )
175
+
176
+ data_parser.add_argument(
177
+ "-c",
178
+ "--components",
179
+ nargs="+",
180
+ help="List of component codes for components to include, "
181
+ "otherwise all components will be included.",
182
+ )
183
+
150
184
 
151
185
  def command(ladok, args):
152
- data_writer = csv.writer(sys.stdout, delimiter=args.delimiter)
153
- course_rounds = filter_rounds(
154
- ladok.search_course_rounds(code=args.course_code),
155
- args.rounds)
156
-
157
- data_writer.writerow([
158
- "Course", "Round", "Component", "Student", "Grade", "Time"
159
- ])
160
- for course_round in course_rounds:
161
- data = extract_data_for_round(ladok, course_round, args)
162
-
163
- for student, component, grade, time in data:
164
- data_writer.writerow(
165
- [course_round.code, course_round.round_code, component,
166
- student, grade, time]
167
- )
186
+ data_writer = csv.writer(sys.stdout, delimiter=args.delimiter)
187
+ course_rounds = filter_rounds(
188
+ ladok.search_course_rounds(code=args.course_code), args.rounds
189
+ )
190
+
191
+ data_writer.writerow(["Course", "Round", "Component", "Student", "Grade", "Time"])
192
+ for course_round in course_rounds:
193
+ data = extract_data_for_round(ladok, course_round, args)
194
+
195
+ for student, component, grade, time in data:
196
+ data_writer.writerow(
197
+ [
198
+ course_round.code,
199
+ course_round.round_code,
200
+ component,
201
+ student,
202
+ grade,
203
+ time,
204
+ ]
205
+ )
ladok3/ladok3.nw CHANGED
@@ -46,6 +46,17 @@ It uses the [[requests]] module.
46
46
  We can use the class as follows.
47
47
  \inputminted{python}{../examples/example_LadokSession.py}
48
48
 
49
+ An alternative way would be to use the general authentication procedure that
50
+ seems to work at most Swedish universities.
51
+ \begin{minted}[linenos]{python}
52
+ import ladok3
53
+
54
+ ladok = ladok3.LadokSession("KTH Royal Institute of Technology",
55
+ vars={"username": os.environ["KTH_LOGIN"],
56
+ "password": os.environ["KTH_PASSWD"]},
57
+ test_environment=True) # for experiments
58
+ \end{minted}
59
+
49
60
  This chapter covers how the [[LadokSession]] class work.
50
61
  The remaining chapters cover what the [[ladok]] object can be used for.
51
62
  \Cref{StudentClasses} covers how we can work with student data.
@@ -1034,27 +1045,126 @@ This is essentially an \enquote{instance of a course syllabus}.
1034
1045
  We know from above that it inherits from [[LadokRemoteData]] and that it must
1035
1046
  be initialized with the keywords [[ladok]] (for its parent) and
1036
1047
  [[UtbildningsinstansUID]] (for itself) and optionally some data.
1037
- This leaves the following methods.
1038
1048
  <<CourseInstance methods>>=
1039
1049
  def __init__(self, /, **kwargs):
1040
1050
  self.__instance_id = kwargs.pop("UtbildningsinstansUID")
1041
- super().__init__(**kwargs)
1051
+ super().__init__(**kwargs) # sets self.ladok
1042
1052
 
1043
- try:
1044
- self.__assign_attr(kwargs)
1045
- except:
1046
- self.__pull_attributes()
1053
+ <<CourseInstance constructor body>>
1054
+ @
1055
+
1056
+ We can fetch the data from LADOK as follows.
1057
+ Note that we need to use the [[round_id]] from the [[CourseRound]] object.
1058
+ This works since we never have a [[CourseInstance]] object on its own, it's
1059
+ always parent of a [[CourseRound]] object.
1060
+ (And that makes this kind of ugly from an OOP perspective.)
1061
+ <<fetch CourseInstance data from LADOK>>=
1062
+ data = self.ladok.course_round_components_JSON(self.round_id)
1063
+ @
1064
+
1065
+ Then data will be populated with the following values:
1066
+ \begin{pycode}
1067
+ import json
1068
+ import ladok3
1069
+ import os
1047
1070
 
1071
+ ladok = ladok3.LadokSession(
1072
+ os.environ["LADOK_INST"],
1073
+ vars={"username": os.environ["LADOK_USER"],
1074
+ "password": os.environ["LADOK_PASS"]},
1075
+ test_environment=True)
1076
+
1077
+ print(r"\begin{minted}{JSON}")
1078
+ data = ladok.course_round_components_JSON(
1079
+ "cf7045a7-3e1c-11eb-b960-5f936a674375")
1080
+ ladok3.clean_data(data)
1081
+ print(json.dumps(data, indent=2))
1082
+ print(r"\end{minted}")
1083
+ \end{pycode}
1084
+
1085
+ Now that we have all data in [[data]], we can assign its values to the private
1086
+ attributes.
1087
+ We note, however, that some course instances lack both [[Versionsnummer]] and
1088
+ [[Omfattning]].
1089
+ It seems like faux courses are created to document when students go on
1090
+ Exchanges.
1091
+ These faux courses have a name and code, but no credits or version of syllabus.
1092
+ Consequently they don't have any grading scales either.
1093
+
1094
+ Our approach will be to try to assign the attributes using [[kwargs]], and if
1095
+ that fails, we will fetch the data (pull from LADOK).
1096
+ If that pull fails with missing data, then we will assume that it's one of
1097
+ these faux courses.
1098
+ <<CourseInstance constructor body>>=
1099
+ try:
1100
+ self.__assign_attr(kwargs)
1101
+ except:
1102
+ self.__pull_attributes()
1103
+ <<CourseInstance methods>>=
1048
1104
  def __assign_attr(self, data):
1049
1105
  <<assign CourseInstance data to private attributes>>
1050
1106
 
1051
1107
  def __pull_attributes(self):
1052
- <<fetch CourseInstance data object from LADOK>>
1053
- self.__assign_attr(data)
1108
+ <<fetch CourseInstance data from LADOK>>
1109
+ try:
1110
+ self.__assign_attr(data)
1111
+ except:
1112
+ self.__assign_faux(data)
1054
1113
 
1055
1114
  def pull(self):
1056
1115
  self.__pull_attributes()
1057
1116
 
1117
+ def __assign_faux(self, data):
1118
+ <<assign faux CourseInstance data to private attributes>>
1119
+ @
1120
+
1121
+ By trial-and-error, it seems like the faux courses has none of the attributes
1122
+ that the real courses have.
1123
+ However, we try our best.
1124
+ <<assign common CourseInstance data to private attributes>>=
1125
+ if "IngaendeMoment" in data:
1126
+ self.__components = [CourseComponent(
1127
+ ladok=self.ladok, course=self,
1128
+ **component) for component in data["IngaendeMoment"]]
1129
+ else:
1130
+ self.__components = []
1131
+ <<assign CourseInstance data to private attributes>>=
1132
+ <<assign common CourseInstance data to private attributes>>
1133
+
1134
+ self.__name = data.pop("Benamning")
1135
+ self.__code = data.pop("Utbildningskod")
1136
+
1137
+ self.__credits = data.pop("Omfattning")
1138
+ self.__unit = data.pop("Enhet")
1139
+
1140
+ self.__version = data.pop("Versionsnummer")
1141
+
1142
+ self.__education_id = data.pop("UtbildningUID")
1143
+
1144
+ self.__grade_scale = self.ladok.get_grade_scales(
1145
+ id=data.pop("BetygsskalaID"))
1146
+ <<assign faux CourseInstance data to private attributes>>=
1147
+ <<assign common CourseInstance data to private attributes>>
1148
+
1149
+ self.__name = data.pop("Benamning", None)
1150
+ self.__code = data.pop("Utbildningskod", None)
1151
+
1152
+ self.__credits = data.pop("Omfattning", None)
1153
+ self.__unit = data.pop("Enhet", None)
1154
+
1155
+ self.__version = data.pop("Versionsnummer", None)
1156
+
1157
+ self.__education_id = data.pop("UtbildningUID", None)
1158
+
1159
+ try:
1160
+ self.__grade_scale = self.ladok.get_grade_scales(
1161
+ id=data.pop("BetygsskalaID"))
1162
+ except KeyError:
1163
+ self.__grade_scale = None
1164
+ @
1165
+
1166
+ Finally, we want to have properties to access the private attributes.
1167
+ <<CourseInstance methods>>=
1058
1168
  @property
1059
1169
  def instance_id(self):
1060
1170
  return self.__instance_id
@@ -1092,57 +1202,6 @@ def components(self, /, **kwargs):
1092
1202
  return filter_on_keys(self.__components, **kwargs)
1093
1203
  @
1094
1204
 
1095
- Now we must fetch data from LADOK.
1096
- <<fetch CourseInstance data object from LADOK>>=
1097
- data = self.ladok.course_round_components_JSON(self.round_id)
1098
- @ Then data will be populated with the following values:
1099
- \begin{pycode}
1100
- import json
1101
- import ladok3
1102
- import os
1103
-
1104
- ladok = ladok3.LadokSession(
1105
- os.environ["LADOK_INST"],
1106
- vars={"username": os.environ["LADOK_USER"],
1107
- "password": os.environ["LADOK_PASS"]},
1108
- test_environment=True)
1109
-
1110
- print(r"\begin{minted}{JSON}")
1111
- data = ladok.course_round_components_JSON(
1112
- "cf7045a7-3e1c-11eb-b960-5f936a674375")
1113
- ladok3.clean_data(data)
1114
- print(json.dumps(data, indent=2))
1115
- print(r"\end{minted}")
1116
- \end{pycode}
1117
-
1118
- Now that we have the [[data]] object, we can assign its values to the private
1119
- attributes.
1120
- We note, however, that some course instances lack both [[Versionsnummer]] and
1121
- [[Omfattning]].
1122
- It seems like faux courses are created to document when students go on
1123
- Exchanges.
1124
- These faux courses have a name and code, but no credits or version of syllabus.
1125
- <<assign CourseInstance data to private attributes>>=
1126
- self.__education_id = data.pop("UtbildningUID")
1127
-
1128
- self.__code = data.pop("Utbildningskod")
1129
- self.__name = data.pop("Benamning")
1130
- self.__version = data.pop("Versionsnummer", None)
1131
-
1132
- self.__credits = data.pop("Omfattning", None)
1133
- self.__unit = data.pop("Enhet", None)
1134
-
1135
- self.__grade_scale = self.ladok.get_grade_scales(
1136
- id=data.pop("BetygsskalaID"))
1137
-
1138
- if "IngaendeMoment" in data:
1139
- self.__components = [CourseComponent(
1140
- ladok=self.ladok, course=self,
1141
- **component) for component in data["IngaendeMoment"]]
1142
- else:
1143
- self.__components = []
1144
- @
1145
-
1146
1205
 
1147
1206
  \section{Course components}
1148
1207
 
ladok3/report.nw CHANGED
@@ -133,7 +133,7 @@ Finally, we have the list of graders.
133
133
  <<add one result group arguments>>=
134
134
  one_parser.add_argument("graders", nargs="*",
135
135
  help="Space separated list of who did the grading, "
136
- "give each grader as 'First Last <email@instutution.se>'.")
136
+ "give each grader as 'First Last <email@institution.se>'.")
137
137
  @
138
138
 
139
139
  Now that we have the arguments, we can just execute the following code using