ygrader 2.3.0__tar.gz → 2.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
@@ -4,7 +4,7 @@ setup(
4
4
  name="ygrader",
5
5
  packages=["ygrader"],
6
6
  package_data={"ygrader": ["*.ahk"]},
7
- version="2.3.0",
7
+ version="2.5.0",
8
8
  description="Grading scripts used in BYU's Electrical and Computer Engineering Department",
9
9
  author="Jeff Goeders",
10
10
  author_email="jeff.goeders@gmail.com",
@@ -5,55 +5,59 @@ import pathlib
5
5
  import zipfile
6
6
  from typing import Callable, Dict, Optional, Tuple
7
7
 
8
- import numpy as np
9
8
  import pandas
9
+ import yaml
10
10
 
11
11
  from .deductions import StudentDeductions
12
12
  from .grading_item_config import LearningSuiteColumn
13
13
  from .utils import warning, print_color, TermColors
14
14
 
15
15
 
16
- # Type alias for late penalty callback: (late_days, max_score, actual_score) -> new_score
17
- LatePenaltyCallback = Callable[[int, float, float], float]
16
+ # Type alias for late penalty callback:
17
+ # (due_datetime, submitted_datetime, max_score, actual_score) -> new_score
18
+ # If submitted_datetime is None, the student submitted on time or no submit time was recorded.
19
+ LatePenaltyCallback = Callable[
20
+ [datetime.datetime, Optional[datetime.datetime], float, float], float
21
+ ]
18
22
 
19
23
 
20
- def _calculate_late_days(
21
- submit_time_str: Optional[str],
22
- due_date: datetime.datetime,
23
- ) -> int:
24
- """Calculate the number of business days late from a submit time.
24
+ def _load_due_date_exceptions(
25
+ exceptions_path: pathlib.Path,
26
+ ) -> Dict[str, datetime.datetime]:
27
+ """Load due date exceptions from YAML file.
25
28
 
26
29
  Args:
27
- submit_time_str: ISO format timestamp string of submission.
28
- due_date: The effective due date for the student.
30
+ exceptions_path: Path to the deadline_exceptions.yaml file.
31
+ Expected format is: net_id: "YYYY-MM-DD HH:MM:SS"
29
32
 
30
33
  Returns:
31
- Number of business days late (0 if on time or no submit time).
34
+ Mapping from net_id to exception datetime.
32
35
  """
33
- if not submit_time_str:
34
- return 0
36
+ if not exceptions_path.exists():
37
+ return {}
35
38
 
36
- try:
37
- submit_time = datetime.datetime.fromisoformat(submit_time_str)
38
- except ValueError:
39
- return 0
39
+ with open(exceptions_path, "r", encoding="utf-8") as f:
40
+ data = yaml.safe_load(f)
40
41
 
41
- if submit_time <= due_date:
42
- return 0
42
+ if not data:
43
+ return {}
43
44
 
44
- days_late = np.busday_count(due_date.date(), submit_time.date())
45
- if days_late == 0:
46
- days_late = 1 # Same day but after deadline
47
- return int(days_late)
45
+ exceptions = {}
46
+ for net_id, exception_date in data.items():
47
+ if net_id and exception_date:
48
+ exceptions[net_id] = datetime.datetime.strptime(
49
+ exception_date, "%Y-%m-%d %H:%M:%S"
50
+ )
51
+ return exceptions
48
52
 
49
53
 
50
- def _get_student_key_and_max_late_days(
54
+ def _get_student_key_and_submit_info(
51
55
  net_id: str,
52
56
  item_deductions: Dict[str, StudentDeductions],
53
57
  due_date: Optional[datetime.datetime] = None,
54
58
  due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
55
- ) -> tuple:
56
- """Find the student key and maximum late days across all items.
59
+ ) -> Tuple[Optional[tuple], Optional[datetime.datetime], Optional[datetime.datetime]]:
60
+ """Find the student key and submission timing info across all items.
57
61
 
58
62
  Args:
59
63
  net_id: The student's net ID.
@@ -62,10 +66,12 @@ def _get_student_key_and_max_late_days(
62
66
  due_date_exceptions: Mapping from net_id to exception due date.
63
67
 
64
68
  Returns:
65
- Tuple of (student_key or None, max_late_days).
69
+ Tuple of (student_key or None, effective_due_date or None, latest_submit_time or None).
70
+ latest_submit_time is None if on time or no submit time recorded.
66
71
  """
67
- max_late_days = 0
68
72
  found_student_key = None
73
+ latest_submit_time: Optional[datetime.datetime] = None
74
+ effective_due_date: Optional[datetime.datetime] = due_date
69
75
 
70
76
  if due_date_exceptions is None:
71
77
  due_date_exceptions = {}
@@ -92,26 +98,35 @@ def _get_student_key_and_max_late_days(
92
98
  if student_key:
93
99
  found_student_key = student_key
94
100
 
95
- # Calculate late days from submit_time if we have a due date
101
+ # Calculate effective due date (using most generous exception for group)
96
102
  if due_date is not None:
97
- submit_time_str = deductions_obj.submit_time_by_students.get(
98
- student_key
99
- )
100
- if submit_time_str:
101
- # Calculate effective due date (using most generous exception for group)
102
- effective_due_date = due_date
103
- for member_net_id in student_key:
104
- if member_net_id in due_date_exceptions:
105
- effective_due_date = max(
106
- effective_due_date, due_date_exceptions[member_net_id]
107
- )
108
-
109
- days_late = _calculate_late_days(
110
- submit_time_str, effective_due_date
111
- )
112
- max_late_days = max(max_late_days, days_late)
113
-
114
- return found_student_key, max_late_days
103
+ effective_due_date = due_date
104
+ for member_net_id in student_key:
105
+ if member_net_id in due_date_exceptions:
106
+ effective_due_date = max(
107
+ effective_due_date, due_date_exceptions[member_net_id]
108
+ )
109
+
110
+ # Get submit time
111
+ submit_time_str = deductions_obj.submit_time_by_students.get(student_key)
112
+ if submit_time_str:
113
+ try:
114
+ submit_time = datetime.datetime.fromisoformat(submit_time_str)
115
+ # Track latest submit time across all items
116
+ if latest_submit_time is None or submit_time > latest_submit_time:
117
+ latest_submit_time = submit_time
118
+ except ValueError:
119
+ pass
120
+
121
+ # Return None for submit_time if on time
122
+ if (
123
+ latest_submit_time is not None
124
+ and effective_due_date is not None
125
+ and latest_submit_time <= effective_due_date
126
+ ):
127
+ latest_submit_time = None
128
+
129
+ return found_student_key, effective_due_date, latest_submit_time
115
130
 
116
131
 
117
132
  def _calculate_student_score(
@@ -123,7 +138,7 @@ def _calculate_student_score(
123
138
  warn_on_missing_callback: bool = True,
124
139
  due_date: Optional[datetime.datetime] = None,
125
140
  due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
126
- ) -> Tuple[float, float, int]:
141
+ ) -> Tuple[float, float, Optional[datetime.datetime]]:
127
142
  """Calculate a student's final score.
128
143
 
129
144
  Args:
@@ -131,12 +146,12 @@ def _calculate_student_score(
131
146
  ls_column: The LearningSuiteColumn configuration.
132
147
  item_deductions: Mapping from item name to StudentDeductions.
133
148
  late_penalty_callback: Optional callback for late penalty.
134
- warn_on_missing_callback: Whether to warn if late days found but no callback.
149
+ warn_on_missing_callback: Whether to warn if late but no callback.
135
150
  due_date: The default due date for the assignment.
136
151
  due_date_exceptions: Mapping from net_id to exception due date.
137
152
 
138
153
  Returns:
139
- Tuple of (final_score, total_possible, max_late_days).
154
+ Tuple of (final_score, total_possible, submitted_datetime or None if on time).
140
155
  """
141
156
  total_possible = sum(item.points for item in ls_column.items)
142
157
  total_deductions = 0.0
@@ -162,21 +177,26 @@ def _calculate_student_score(
162
177
  # Calculate score before late penalty
163
178
  score = max(0, total_possible - total_deductions)
164
179
 
165
- # Get max late days
166
- _, max_late_days = _get_student_key_and_max_late_days(
180
+ # Get submit info
181
+ _, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
167
182
  net_id, item_deductions, due_date, due_date_exceptions
168
183
  )
169
184
 
170
- # Apply late penalty if applicable
171
- if max_late_days > 0:
172
- if late_penalty_callback:
173
- score = max(0, late_penalty_callback(max_late_days, total_possible, score))
185
+ # Apply late penalty if applicable (submitted_datetime is None if on time)
186
+ if submitted_datetime is not None:
187
+ if late_penalty_callback and effective_due_date is not None:
188
+ score = max(
189
+ 0,
190
+ late_penalty_callback(
191
+ effective_due_date, submitted_datetime, total_possible, score
192
+ ),
193
+ )
174
194
  elif warn_on_missing_callback:
175
195
  warning(
176
- f"Student {net_id} has {max_late_days} late day(s) but no late penalty callback provided"
196
+ f"Student {net_id} submitted late but no late penalty callback provided"
177
197
  )
178
198
 
179
- return score, total_possible, max_late_days
199
+ return score, total_possible, submitted_datetime
180
200
 
181
201
 
182
202
  def assemble_grades(
@@ -188,7 +208,7 @@ def assemble_grades(
188
208
  output_csv_path: Optional[pathlib.Path] = None,
189
209
  late_penalty_callback: Optional[LatePenaltyCallback] = None,
190
210
  due_date: Optional[datetime.datetime] = None,
191
- due_date_exceptions: Optional[Dict[str, datetime.datetime]] = None,
211
+ due_date_exceptions_path: Optional[pathlib.Path] = None,
192
212
  ) -> Tuple[Optional[pathlib.Path], Optional[pathlib.Path]]:
193
213
  """Generate feedback zip and/or grades CSV from deductions.
194
214
 
@@ -199,14 +219,20 @@ def assemble_grades(
199
219
  output_zip_path: Path for the output zip file. If None, no zip is generated.
200
220
  output_csv_path: Path for the output CSV file. If None, no CSV is generated.
201
221
  late_penalty_callback: Optional callback function that takes
202
- (late_days, max_score, actual_score) and returns the adjusted score.
222
+ (due_datetime, submitted_datetime, max_score, actual_score) and returns the adjusted score.
223
+ submitted_datetime will be None if on time.
203
224
  due_date: The default due date for the assignment. Required for late penalty.
204
- due_date_exceptions: Mapping from net_id to exception due date.
225
+ due_date_exceptions_path: Path to YAML file with due date exceptions (net_id: "YYYY-MM-DD HH:MM:SS").
205
226
 
206
227
  Returns:
207
228
  Tuple of (feedback_zip_path or None, grades_csv_path or None).
208
229
  """
209
230
  yaml_path = pathlib.Path(yaml_path)
231
+
232
+ # Load due date exceptions if path provided
233
+ due_date_exceptions: Dict[str, datetime.datetime] = {}
234
+ if due_date_exceptions_path:
235
+ due_date_exceptions = _load_due_date_exceptions(due_date_exceptions_path)
210
236
  ls_column = LearningSuiteColumn(yaml_path)
211
237
 
212
238
  # Get the lab name from the YAML file's parent directory
@@ -263,8 +289,13 @@ def assemble_grades(
263
289
  f"but NOT [{', '.join(items_not_graded)}]",
264
290
  )
265
291
 
292
+ # Get submit info for this student
293
+ _, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
294
+ net_id, subitem_deductions, due_date, due_date_exceptions
295
+ )
296
+
266
297
  # Calculate score before late penalty
267
- score_before_late, total_possible, max_late_days = _calculate_student_score(
298
+ score_before_late, total_possible, _ = _calculate_student_score(
268
299
  net_id=net_id,
269
300
  ls_column=ls_column,
270
301
  item_deductions=subitem_deductions,
@@ -274,18 +305,25 @@ def assemble_grades(
274
305
  due_date_exceptions=due_date_exceptions,
275
306
  )
276
307
 
277
- # Apply late penalty if applicable
308
+ # Apply late penalty if applicable (submitted_datetime is None if on time)
278
309
  final_score = score_before_late
279
- if max_late_days > 0 and late_penalty_callback:
310
+ if (
311
+ submitted_datetime is not None
312
+ and late_penalty_callback
313
+ and effective_due_date is not None
314
+ ):
280
315
  final_score = max(
281
316
  0,
282
317
  late_penalty_callback(
283
- max_late_days, total_possible, score_before_late
318
+ effective_due_date,
319
+ submitted_datetime,
320
+ total_possible,
321
+ score_before_late,
284
322
  ),
285
323
  )
286
324
  print_color(
287
325
  TermColors.YELLOW,
288
- f"Late: {net_id} ({max_late_days} day{'s' if max_late_days != 1 else ''}): "
326
+ f"Late: {net_id} (submitted {submitted_datetime}): "
289
327
  f"{score_before_late:.1f} -> {final_score:.1f}",
290
328
  )
291
329
 
@@ -433,24 +471,26 @@ def _generate_student_feedback(
433
471
  # Calculate score before late penalty (clamped to 0)
434
472
  score_before_late = max(0, total_points_possible - total_points_deducted)
435
473
 
436
- # Get max late days for this student
437
- _, max_late_days = _get_student_key_and_max_late_days(
474
+ # Get submit info for this student
475
+ _, effective_due_date, submitted_datetime = _get_student_key_and_submit_info(
438
476
  net_id, subitem_deductions, due_date, due_date_exceptions
439
477
  )
440
478
 
441
479
  # Late penalty section
442
480
  lines.append("")
443
481
  lines.append("=" * 60)
444
- if max_late_days > 0 and late_penalty_callback:
482
+ if (
483
+ submitted_datetime is not None
484
+ and late_penalty_callback
485
+ and effective_due_date is not None
486
+ ):
445
487
  final_score = late_penalty_callback(
446
- max_late_days, total_points_possible, score_before_late
488
+ effective_due_date, submitted_datetime, total_points_possible, score_before_late
447
489
  )
448
490
  # Ensure final score is not negative
449
491
  final_score = max(0, final_score)
450
492
  late_penalty_points = score_before_late - final_score
451
- late_label = (
452
- f"Late Penalty ({max_late_days} day{'s' if max_late_days != 1 else ''}):"
453
- )
493
+ late_label = f"Late Penalty (submitted {submitted_datetime.strftime('%Y-%m-%d %H:%M')}):"
454
494
  lines.append(
455
495
  f"{late_label:<{item_col_width}} {-late_penalty_points:>{score_col_width}.1f}"
456
496
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ygrader
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Grading scripts used in BYU's Electrical and Computer Engineering Department
5
5
  Home-page: https://github.com/byu-cpe/ygrader
6
6
  Author: Jeff Goeders
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes