ladok3 4.10__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/report.nw CHANGED
@@ -32,6 +32,14 @@ import sys
32
32
  <<functions>>
33
33
 
34
34
  def add_command_options(parser):
35
+ """Add the 'report' subcommand options to the argument parser.
36
+
37
+ Creates a subparser for result reporting with options for single results
38
+ via command line arguments or batch processing via CSV input.
39
+
40
+ Args:
41
+ parser (ArgumentParser): The parent parser to add the subcommand to.
42
+ """
35
43
  report_parser = parser.add_parser("report",
36
44
  help="Reports course results to LADOK",
37
45
  description="Reports course results to LADOK"
@@ -40,6 +48,12 @@ def add_command_options(parser):
40
48
  <<add report command arguments to report parser>>
41
49
 
42
50
  def command(ladok, args):
51
+ """Execute the result reporting command.
52
+
53
+ Args:
54
+ ladok (LadokSession): The LADOK session for data access.
55
+ args: Parsed command line arguments containing result data or CSV options.
56
+ """
43
57
  <<report results depending on args>>
44
58
  @
45
59
 
@@ -60,9 +74,21 @@ many_parser = report_parser.add_argument_group(
60
74
  We provide two functions, one for each way of reporting the results.
61
75
  <<functions>>=
62
76
  def report_one_result(ladok, args):
77
+ """Report a single result specified via command line arguments.
78
+
79
+ Args:
80
+ ladok (LadokSession): The LADOK session for data access.
81
+ args: Command line arguments containing course, student, component, and grade info.
82
+ """
63
83
  <<report results given in args>>
64
84
 
65
85
  def report_many_results(ladok, args):
86
+ """Report multiple results read from CSV data on stdin.
87
+
88
+ Args:
89
+ ladok (LadokSession): The LADOK session for data access.
90
+ args: Command line arguments containing CSV processing options.
91
+ """
66
92
  <<report results given in stdin>>
67
93
  @
68
94
 
@@ -83,6 +109,141 @@ report_parser.add_argument("-f", "--finalize",
83
109
  @
84
110
 
85
111
 
112
+ \section{Report many results given in standard input}
113
+
114
+ We want to read CSV data from standard input.
115
+ <<report results given in stdin>>=
116
+ data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
117
+ for course_code, component_code, student_id, grade, date, *graders in data_reader:
118
+ <<report a result read from stdin>>
119
+ @ We need to add an argument for the delimiter.
120
+ <<add many results group arguments>>=
121
+ many_parser.add_argument("-d", "--delimiter",
122
+ default="\t",
123
+ help="The delimiter for the CSV input; "
124
+ "default is a tab character to be compatible with POSIX commands, "
125
+ "use `-d,` or `-d ,` to get comma-separated values.")
126
+ @
127
+
128
+ We also want to handle errors and confirmations.
129
+ When reporting in bulk, we don't want unnecessary errors.
130
+ We also want to have a summary of the changes.
131
+ <<add many results group arguments>>=
132
+ many_parser.add_argument("-v", "--verbose",
133
+ action="count", default=0,
134
+ help="Increases the verbosity of the output: -v will print results that "
135
+ "were reported to standard out. Otherwise only errors are printed.")
136
+ @
137
+
138
+ Then we can actually report the result using the values read from [[stdin]].
139
+ <<report a result read from stdin>>=
140
+ try:
141
+ set_grade(ladok, args,
142
+ student_id, course_code, component_code, grade, date, graders)
143
+ except Exception as err:
144
+ <<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
145
+ print(f"{course_code} {component_code}={grade} ({date}) {student}: "
146
+ f"{err}",
147
+ file=sys.stderr)
148
+ @
149
+
150
+ The reason we want to resolve the student from LADOK is that the [[student_id]]
151
+ might be a personnummer or a LADOK ID---if the latter, it's not particularly
152
+ readable for a human and we can't use the LADOK ID in the LADOK web interface
153
+ when we want to deal with the errors manually.
154
+ But if we resolve the student, then we get a readable name.
155
+ <<try to resolve [[student]] from [[ladok]] using [[student_id]]>>=
156
+ try:
157
+ student = ladok.get_student(student_id)
158
+ except Exception:
159
+ student = student_id
160
+ @
161
+
162
+ When we set the grade, there are a few cases that should be handled.
163
+ If the grade isn't attested, we try to change it.
164
+ (This might still fail if the grade is finalized but not attested.)
165
+ If we've selected the verbose option, then we print what we have reported.
166
+
167
+ If the grade was attested, then we check if it's different.
168
+ If it's different, we output this.
169
+ If it's the same, we silently ignore it.
170
+ This is best for bulk reporting, because then we can always try to report for
171
+ all students.
172
+
173
+ We want to report errors as exceptions.
174
+ <<functions>>=
175
+ def set_grade(ladok, args,
176
+ student_id, course_code, component_code, grade, date, graders):
177
+ """Set a grade for a student's course component result.
178
+
179
+ Handles the logic for setting grades, including checking if results are
180
+ already attested, validating dates, and optionally finalizing results.
181
+
182
+ Args:
183
+ ladok (LadokSession): The LADOK session for data access.
184
+ args: Command line arguments with options like verbose and finalize.
185
+ student_id (str): Student identifier (personnummer or LADOK ID).
186
+ course_code (str): Course code (e.g., "DD1315").
187
+ component_code (str): Component code (e.g., "LAB1").
188
+ grade (str): Grade to assign (e.g., "P", "F", "A").
189
+ date (str): Examination date in YYYY-MM-DD format.
190
+ graders (list): List of grader identifiers.
191
+
192
+ Raises:
193
+ Exception: If the grade setting fails or validation errors occur.
194
+ """
195
+ student = ladok.get_student(student_id)
196
+ <<get [[course]] from [[student]] and [[course_code]]>>
197
+ <<get [[component]] from [[course]] and [[component_code]]>>
198
+
199
+ if not component.attested and component.grade != grade:
200
+ <<ensure [[date]] is a valid date for [[course]]>>
201
+ component.set_grade(grade, date)
202
+ if args.finalize:
203
+ component.finalize(graders)
204
+ if args.verbose:
205
+ print(f"{course_code} {student}: reported "
206
+ f"{component.component} = {component.grade} ({date}) "
207
+ f"by {', '.join(graders)}.")
208
+ elif component.grade != grade:
209
+ raise LadokValidationError(f"attested {component.component} "
210
+ f"result {component.grade} ({component.date}) "
211
+ f"is different from {grade} ({date}).")
212
+ @
213
+
214
+ Now we simply want to set those objects up.
215
+ We want to throw exceptions that explain what the problem is if these don't
216
+ exist.
217
+ <<get [[course]] from [[student]] and [[course_code]]>>=
218
+ try:
219
+ course = student.courses(code=course_code)[0]
220
+ except IndexError:
221
+ raise LadokNotFoundError(f"{course_code}: No such course for {student}")
222
+ <<get [[component]] from [[course]] and [[component_code]]>>=
223
+ try:
224
+ component = course.results(component=component_code)[0]
225
+ except IndexError:
226
+ raise LadokNotFoundError(f"{component_code}: no such component for {course_code}")
227
+ @
228
+
229
+ Finally, we want to ensure the date is correct.
230
+ The date must be at the earliest the start of the course.
231
+ The student can't finish any results before the course has started.
232
+ LADOK will not accept that.
233
+ <<ensure [[date]] is a valid date for [[course]]>>=
234
+ if not isinstance(date, datetime.date):
235
+ date = datetime.date.fromisoformat(date)
236
+
237
+ if date < course.start:
238
+ print(f"{course_code} {component_code}={grade} "
239
+ f"({date}) {student}: "
240
+ f"Grade date ({date}) is before "
241
+ f"course start date ({course.start}), "
242
+ f"using course start date instead.")
243
+ date = course.start
244
+ @
245
+
246
+
86
247
  \section{Report a result given on command line}
87
248
 
88
249
  If we've chosen to give one result on the command line, then we'll need the
@@ -91,28 +252,33 @@ following arguments.
91
252
  We start with the course, component code, the student's ID and grade.
92
253
  <<add one result group arguments>>=
93
254
  one_parser.add_argument("course_code", nargs="?",
94
- help="The course code (e.g. DD1315) for which the grade is for"
255
+ help="The course code (e.g. DD1315) for which the grade is for."
95
256
  )
96
257
 
97
258
  one_parser.add_argument("component_code", nargs="?",
98
- help="The component code (e.g. LAB1) for which the grade is for"
259
+ help="The component code (e.g. LAB1) for which the grade is for. "
260
+ "This can be set to the course code (e.g. DD1315) to set the "
261
+ "final grade for the course. But all components must be "
262
+ "certified (attested) before the course grade can be set."
99
263
  )
100
264
 
101
265
  one_parser.add_argument("student_id", nargs="?",
102
- help="Student identifier (personnummer or LADOK ID)"
266
+ help="Student identifier (personnummer or LADOK ID)."
103
267
  )
104
268
 
105
269
  one_parser.add_argument("grade", nargs="?",
106
- help="The grade (e.g. A or P)"
270
+ help="The grade (e.g. A or P)."
107
271
  )
108
- @ We must make them optional like this to make it work with out second
109
- alternative, so we must check ourselves that we got the arguments.
272
+ @ We must make them optional like this to make it work with our second
273
+ alternative (bulk reporting through [[stdin]]), so we must check ourselves that
274
+ we got the arguments.
110
275
  <<check that we got all positional arguments>>=
111
276
  if not (args.course_code and args.component_code and
112
277
  args.student_id and args.grade):
113
278
  print(f"{sys.argv[0]} report: "
114
- "not all positional args given: course_code, component, student, grade",
115
- file=sys.stderr)
279
+ "not all positional args given: "
280
+ "course_code, component, student, grade",
281
+ file=sys.stderr)
116
282
  sys.exit(1)
117
283
  @
118
284
 
@@ -123,7 +289,7 @@ If it's not provided, we let [[argparse]] set it to today's date.
123
289
  <<add one result group arguments>>=
124
290
  one_parser.add_argument("date", nargs="?",
125
291
  help="Date on ISO format (e.g. 2021-03-18), "
126
- f"defaults to today's date ({datetime.date.today()})",
292
+ f"defaults to today's date ({datetime.date.today()}).",
127
293
  type=datetime.date.fromisoformat,
128
294
  default=datetime.date.today()
129
295
  )
@@ -141,115 +307,15 @@ them.
141
307
  <<report results given in args>>=
142
308
  <<check that we got all positional arguments>>
143
309
  try:
144
- student = ladok.get_student(args.student_id)
145
- <<look up [[course]] from [[student]]>>
146
- <<look up [[result]] from [[course]]>>
147
- result.set_grade(args.grade, args.date)
148
- if args.finalize:
149
- result.finalize(args.graders)
310
+ set_grade(ladok, args,
311
+ args.student_id, args.course_code, args.component_code,
312
+ args.grade, args.date, args.graders)
150
313
  except Exception as err:
151
- try:
152
- print(f"{student}: {err}")
153
- except ValueError as verr:
154
- print(f"{verr}: {args.student_id}: {err}")
155
- @ The option [[args.finalize]] is already set above, so we don't need to add
156
- that one here.
157
-
158
- Both looking up the course and the component can yield index errors.
159
- We'd like to distinguish these.
160
- We will catch the exception, the reraise the same exception but with a better
161
- error message.
162
- <<look up [[course]] from [[student]]>>=
163
- try:
164
- course = student.courses(code=args.course_code)[0]
165
- except IndexError:
166
- raise Exception("f{args.course_code}: No such course for {student}")
167
- <<look up [[result]] from [[course]]>>=
168
- try:
169
- result = course.results(component=args.component_code)[0]
170
- except IndexError:
171
- raise Exception(f"{args.component_code}: "
172
- f"No such component for {args.course_code}")
173
- @
174
-
175
-
176
- \section{Report many results given in standard input}
177
-
178
- We want to read CSV data from standard input.
179
- <<report results given in stdin>>=
180
- data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
181
- for course_code, component_code, student_id, grade, date, *graders in data_reader:
182
- <<report a result read from stdin>>
183
- @ We need to add an argument for the delimiter.
184
- <<add many results group arguments>>=
185
- many_parser.add_argument("-d", "--delimiter",
186
- default="\t",
187
- help="The delimiter for the CSV input; "
188
- "default is a tab character to be compatible with POSIX commands, "
189
- "use `-d,` or `-d ,` to get comma-separated values.")
190
- @
191
-
192
- We also want to handle errors and confirmations differently.
193
- When reporting in bulk, we don't want unnecessary errors.
194
- We also want to have a summary of the changes.
195
- <<add many results group arguments>>=
196
- many_parser.add_argument("-v", "--verbose",
197
- action="count", default=0,
198
- help="Increases the verbosity of the output: -v will print results that "
199
- "were reported to standard out. Otherwise only errors are printed.")
200
- @
201
-
202
- When we actually report a result, this is similar to how we did it above.
203
- The difference is that we've read the variables from the CSV data in [[stdin]]
204
- instead of from [[args]].
205
- <<report a result read from stdin>>=
206
- try:
207
- student = ladok.get_student(student_id)
208
-
209
- try:
210
- course = student.courses(code=course_code)[0]
211
- except IndexError:
212
- raise Exception(f"{course_code}: No such course for {student}")
213
-
214
- try:
215
- component = course.results(component=component_code)[0]
216
- except IndexError:
217
- raise Exception(f"{component_code}: no such component for {course_code}")
218
-
219
- <<set [[grade]] to [[component]], output if verbose>>
220
- except Exception as err:
221
- try:
222
- print(f"{course_code} {component_code}={grade} ({date}) {student}: {err}",
223
- file=sys.stderr)
224
- except ValueError as verr:
225
- print(f"{verr}: "
226
- f"{course_code} {component_code}={grade} ({date}) {student_id}: {err}",
227
- file=sys.stderr)
228
- @
229
-
230
- Now, when we set the grade, there are a few cases that should be handled.
231
- If the grade isn't attested, we try to change it.
232
- (This might still fail if the grade is finalized but not attested.)
233
- If we've selected the verbose option, then we print what we have reported.
234
-
235
- If the grade was attested, then we check if it's different.
236
- If it's different, we output this.
237
- If it's the same, we silently ignore it.
238
- This is best for bulk reporting, because then we can always try to report for
239
- all students.
240
- <<set [[grade]] to [[component]], output if verbose>>=
241
- if not component.attested and component.grade != grade:
242
- component.set_grade(grade, date)
243
- if args.finalize:
244
- component.finalize(graders)
245
- if args.verbose:
246
- print(f"{course_code} {student}: reported "
247
- f"{component.component} = {component.grade} ({date}) "
248
- f"by {', '.join(graders)}.")
249
- elif component.grade != grade:
250
- print(f"{course_code} {student}: attested {component.component} "
251
- f"result {component.grade} ({component.date}) "
252
- f"is different from {grade} ({date}).")
314
+ student_id = args.student_id
315
+ <<try to resolve [[student]] from [[ladok]] using [[student_id]]>>
316
+ print(f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
317
+ f"{student}: {err}",
318
+ file=sys.stderr)
253
319
  @
254
320
 
255
321
 
ladok3/report.py CHANGED
@@ -5,86 +5,149 @@ import sys
5
5
 
6
6
 
7
7
  def report_one_result(ladok, args):
8
+ """Report a single result specified via command line arguments.
9
+
10
+ Args:
11
+ ladok (LadokSession): The LADOK session for data access.
12
+ args: Command line arguments containing course, student, component, and grade info.
13
+ """
8
14
  if not (
9
15
  args.course_code and args.component_code and args.student_id and args.grade
10
16
  ):
11
17
  print(
12
18
  f"{sys.argv[0]} report: "
13
- "not all positional args given: course_code, component, student, grade",
19
+ "not all positional args given: "
20
+ "course_code, component, student, grade",
14
21
  file=sys.stderr,
15
22
  )
16
23
  sys.exit(1)
17
24
  try:
18
- student = ladok.get_student(args.student_id)
19
- try:
20
- course = student.courses(code=args.course_code)[0]
21
- except IndexError:
22
- raise Exception("f{args.course_code}: No such course for {student}")
23
- try:
24
- result = course.results(component=args.component_code)[0]
25
- except IndexError:
26
- raise Exception(
27
- f"{args.component_code}: " f"No such component for {args.course_code}"
28
- )
29
- result.set_grade(args.grade, args.date)
30
- if args.finalize:
31
- result.finalize(args.graders)
25
+ set_grade(
26
+ ladok,
27
+ args,
28
+ args.student_id,
29
+ args.course_code,
30
+ args.component_code,
31
+ args.grade,
32
+ args.date,
33
+ args.graders,
34
+ )
32
35
  except Exception as err:
36
+ student_id = args.student_id
33
37
  try:
34
- print(f"{student}: {err}")
35
- except ValueError as verr:
36
- print(f"{verr}: {args.student_id}: {err}")
38
+ student = ladok.get_student(student_id)
39
+ except Exception:
40
+ student = student_id
41
+ print(
42
+ f"{args.course_code} {args.component_code}={args.grade} ({args.date}) "
43
+ f"{student}: {err}",
44
+ file=sys.stderr,
45
+ )
37
46
 
38
47
 
39
48
  def report_many_results(ladok, args):
49
+ """Report multiple results read from CSV data on stdin.
50
+
51
+ Args:
52
+ ladok (LadokSession): The LADOK session for data access.
53
+ args: Command line arguments containing CSV processing options.
54
+ """
40
55
  data_reader = csv.reader(sys.stdin, delimiter=args.delimiter)
41
56
  for course_code, component_code, student_id, grade, date, *graders in data_reader:
42
57
  try:
43
- student = ladok.get_student(student_id)
44
-
45
- try:
46
- course = student.courses(code=course_code)[0]
47
- except IndexError:
48
- raise Exception(f"{course_code}: No such course for {student}")
49
-
50
- try:
51
- component = course.results(component=component_code)[0]
52
- except IndexError:
53
- raise Exception(
54
- f"{component_code}: no such component for {course_code}"
55
- )
56
-
57
- if not component.attested and component.grade != grade:
58
- component.set_grade(grade, date)
59
- if args.finalize:
60
- component.finalize(graders)
61
- if args.verbose:
62
- print(
63
- f"{course_code} {student}: reported "
64
- f"{component.component} = {component.grade} ({date}) "
65
- f"by {', '.join(graders)}."
66
- )
67
- elif component.grade != grade:
68
- print(
69
- f"{course_code} {student}: attested {component.component} "
70
- f"result {component.grade} ({component.date}) "
71
- f"is different from {grade} ({date})."
72
- )
58
+ set_grade(
59
+ ladok,
60
+ args,
61
+ student_id,
62
+ course_code,
63
+ component_code,
64
+ grade,
65
+ date,
66
+ graders,
67
+ )
73
68
  except Exception as err:
74
69
  try:
75
- print(
76
- f"{course_code} {component_code}={grade} ({date}) {student}: {err}",
77
- file=sys.stderr,
78
- )
79
- except ValueError as verr:
80
- print(
81
- f"{verr}: "
82
- f"{course_code} {component_code}={grade} ({date}) {student_id}: {err}",
83
- file=sys.stderr,
84
- )
70
+ student = ladok.get_student(student_id)
71
+ except Exception:
72
+ student = student_id
73
+ print(
74
+ f"{course_code} {component_code}={grade} ({date}) {student}: " f"{err}",
75
+ file=sys.stderr,
76
+ )
77
+
78
+
79
+ def set_grade(
80
+ ladok, args, student_id, course_code, component_code, grade, date, graders
81
+ ):
82
+ """Set a grade for a student's course component result.
83
+
84
+ Handles the logic for setting grades, including checking if results are
85
+ already attested, validating dates, and optionally finalizing results.
86
+
87
+ Args:
88
+ ladok (LadokSession): The LADOK session for data access.
89
+ args: Command line arguments with options like verbose and finalize.
90
+ student_id (str): Student identifier (personnummer or LADOK ID).
91
+ course_code (str): Course code (e.g., "DD1315").
92
+ component_code (str): Component code (e.g., "LAB1").
93
+ grade (str): Grade to assign (e.g., "P", "F", "A").
94
+ date (str): Examination date in YYYY-MM-DD format.
95
+ graders (list): List of grader identifiers.
96
+
97
+ Raises:
98
+ Exception: If the grade setting fails or validation errors occur.
99
+ """
100
+ student = ladok.get_student(student_id)
101
+ try:
102
+ course = student.courses(code=course_code)[0]
103
+ except IndexError:
104
+ raise LadokNotFoundError(f"{course_code}: No such course for {student}")
105
+ try:
106
+ component = course.results(component=component_code)[0]
107
+ except IndexError:
108
+ raise LadokNotFoundError(
109
+ f"{component_code}: no such component for {course_code}"
110
+ )
111
+
112
+ if not component.attested and component.grade != grade:
113
+ if not isinstance(date, datetime.date):
114
+ date = datetime.date.fromisoformat(date)
115
+
116
+ if date < course.start:
117
+ print(
118
+ f"{course_code} {component_code}={grade} "
119
+ f"({date}) {student}: "
120
+ f"Grade date ({date}) is before "
121
+ f"course start date ({course.start}), "
122
+ f"using course start date instead."
123
+ )
124
+ date = course.start
125
+ component.set_grade(grade, date)
126
+ if args.finalize:
127
+ component.finalize(graders)
128
+ if args.verbose:
129
+ print(
130
+ f"{course_code} {student}: reported "
131
+ f"{component.component} = {component.grade} ({date}) "
132
+ f"by {', '.join(graders)}."
133
+ )
134
+ elif component.grade != grade:
135
+ raise LadokValidationError(
136
+ f"attested {component.component} "
137
+ f"result {component.grade} ({component.date}) "
138
+ f"is different from {grade} ({date})."
139
+ )
85
140
 
86
141
 
87
142
  def add_command_options(parser):
143
+ """Add the 'report' subcommand options to the argument parser.
144
+
145
+ Creates a subparser for result reporting with options for single results
146
+ via command line arguments or batch processing via CSV input.
147
+
148
+ Args:
149
+ parser (ArgumentParser): The parent parser to add the subcommand to.
150
+ """
88
151
  report_parser = parser.add_parser(
89
152
  "report",
90
153
  help="Reports course results to LADOK",
@@ -97,25 +160,28 @@ def add_command_options(parser):
97
160
  one_parser.add_argument(
98
161
  "course_code",
99
162
  nargs="?",
100
- help="The course code (e.g. DD1315) for which the grade is for",
163
+ help="The course code (e.g. DD1315) for which the grade is for.",
101
164
  )
102
165
 
103
166
  one_parser.add_argument(
104
167
  "component_code",
105
168
  nargs="?",
106
- help="The component code (e.g. LAB1) for which the grade is for",
169
+ help="The component code (e.g. LAB1) for which the grade is for. "
170
+ "This can be set to the course code (e.g. DD1315) to set the "
171
+ "final grade for the course. But all components must be "
172
+ "certified (attested) before the course grade can be set.",
107
173
  )
108
174
 
109
175
  one_parser.add_argument(
110
- "student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)"
176
+ "student_id", nargs="?", help="Student identifier (personnummer or LADOK ID)."
111
177
  )
112
178
 
113
- one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P)")
179
+ one_parser.add_argument("grade", nargs="?", help="The grade (e.g. A or P).")
114
180
  one_parser.add_argument(
115
181
  "date",
116
182
  nargs="?",
117
183
  help="Date on ISO format (e.g. 2021-03-18), "
118
- f"defaults to today's date ({datetime.date.today()})",
184
+ f"defaults to today's date ({datetime.date.today()}).",
119
185
  type=datetime.date.fromisoformat,
120
186
  default=datetime.date.today(),
121
187
  )
@@ -156,6 +222,12 @@ def add_command_options(parser):
156
222
 
157
223
 
158
224
  def command(ladok, args):
225
+ """Execute the result reporting command.
226
+
227
+ Args:
228
+ ladok (LadokSession): The LADOK session for data access.
229
+ args: Parsed command line arguments containing result data or CSV options.
230
+ """
159
231
  if args.course_code:
160
232
  report_one_result(ladok, args)
161
233
  else: