PyKubeGrader 0.3.7__py3-none-any.whl → 0.3.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyKubeGrader
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -26,6 +26,7 @@ Requires-Dist: ruff
26
26
  Requires-Dist: setuptools
27
27
  Requires-Dist: sphinx
28
28
  Requires-Dist: types-python-dateutil
29
+ Requires-Dist: types-pyyaml
29
30
  Requires-Dist: types-requests
30
31
  Requires-Dist: types-setuptools
31
32
  Provides-Extra: testing
@@ -1,24 +1,30 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
- pykubegrader/grading_tester.ipynb,sha256=wwT9jyhpR6GGM8r4todaGfrsUxS6JxM0qIqMcDYKM7w,18839
2
+ pykubegrader/grading_tester.ipynb,sha256=D26O0fgIV1aIYDnIJQspu72lOOQfNS_boIx93apjKDQ,20025
3
3
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
4
- pykubegrader/telemetry.py,sha256=vZK9p3XqnqacwtiVyZgjI2mcIr5ZcxRRwW5sAZsrJkE,16631
4
+ pykubegrader/telemetry.py,sha256=0Is-LhpVoKcEjmgqEjCfNLCKJVGAmJaQmD2VHF8KI9w,16396
5
5
  pykubegrader/utils.py,sha256=jlJklKvRhY3O7Hz2aaU1m0y3p_n9eMAXNnAF7LUEaPY,1275
6
6
  pykubegrader/validate.py,sha256=OKnItGyd-L8QPKcsE0KRuwBI_IxKiJzMLJKZiA2j3II,11184
7
7
  pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
- pykubegrader/build/api_notebook_builder.py,sha256=EZG4Ow4YATzOWPPNLkdQEdWt7hkpbaI5ZD1Bf2KEWeY,25622
9
- pykubegrader/build/build_folder.py,sha256=HDccW597shBhAd-jxvMUPpsiNj2S4DlKRGMDH_6U9nQ,87590
8
+ pykubegrader/build/api_notebook_builder.py,sha256=FVLnycCRLmZKbimw1FVwDVO-ufKUiAkcnb0O1NrUCEk,25997
9
+ pykubegrader/build/build_folder.py,sha256=ecriZoXp5SzpCvP532vXfox61gyJOg9YAYNVoiNq4Ck,87803
10
10
  pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
11
11
  pykubegrader/build/collate.py,sha256=cVvF7tf2U3iiH4R_dbghTcieedIx5w3Fyw9L_llInM8,6754
12
12
  pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
13
- pykubegrader/grade_reports/grade_reports.py,sha256=n8H_n9jdZRSPn2zlIf-GQt_Y8w91p6M8ZbdVH76Sg5k,2303
13
+ pykubegrader/grade_reports/__grade_reports.py,sha256=n8H_n9jdZRSPn2zlIf-GQt_Y8w91p6M8ZbdVH76Sg5k,2303
14
+ pykubegrader/grade_reports/assignments.py,sha256=Wr5jvTeQ0a7Fl89e6EzXKqjhLrsA6KyObOdrJdN1V0Y,7614
15
+ pykubegrader/grade_reports/class_grade_report.py,sha256=9Uuvy-xoABtjTjv-05VYhmowebgTrgEFSgh1Grws-cQ,5078
16
+ pykubegrader/grade_reports/grade_report.py,sha256=wxgSGjvX9rcE0s9Up2zxPACGv78sJ3TvP8px3bc7t-c,13993
17
+ pykubegrader/grade_reports/grading_config.py,sha256=_Wki6ju6OkqIuwEl-YMpYJcG5Yeu5wid-NEUN5Oxshs,2332
18
+ pykubegrader/grade_reports/test.ipynb,sha256=NkMZHcfBd-uJw3i1Y9ux-epBP5GiVFEhda5wxEbK0cU,808
14
19
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
15
20
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
16
21
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
17
22
  pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
18
23
  pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
19
- pykubegrader/submit/submit_assignment.py,sha256=Cx9hc2A--ts95dFLvn7Fw--ekRwPXqHKKVChnGFgw_Y,2670
20
- pykubegrader/tokens/token_panel.py,sha256=NNA5ZV3Q9jB_lz2aSwMyViXV0ESu6V_7T92Qji7UpSQ,1377
21
- pykubegrader/tokens/tokens.py,sha256=qcYMFgNPimbfeS7lXOtbgquGgeJCgOGx5hvXewIs0oQ,1474
24
+ pykubegrader/submit/submit_assignment.py,sha256=73bbCYNfeWHjxZw-TUuva737WEPQNA9DB6czeP5BIOM,2656
25
+ pykubegrader/tokens/generator.py,sha256=zqfO8GX4Xkhkxi0HGkGwiwuXTmL4ON_hcH8f-9Gg5qc,2648
26
+ pykubegrader/tokens/token_panel.py,sha256=jWwOUx4mr67iWyoIyjITMtwf9HtbAd_L5b-2x_Fif3g,1377
27
+ pykubegrader/tokens/tokens.py,sha256=5hEcNMUCpFtps0OjWV1V93zdk2cuIAwCfhBEp3PiuEI,1473
22
28
  pykubegrader/tokens/validate_token.py,sha256=kvHX0NJBm21xzb2p67j7vq1La6J1XbmobEJQ3fTMdZA,3289
23
29
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
30
  pykubegrader/widgets/multiple_choice.py,sha256=ag6W-HN7isHkIUmB4BxtK8T1JhuV3FBLUBAhcV6rN80,2729
@@ -30,12 +36,12 @@ pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs
30
36
  pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
31
37
  pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
32
38
  pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
33
- pykubegrader/widgets_base/multi_select.py,sha256=KtfAP0PyEbcjWlKNpI5_5-PLMtcUbbNX0Es_-w-H34Q,4226
39
+ pykubegrader/widgets_base/multi_select.py,sha256=WhpS7a8V3BOuEfEyFPzcDhMbgr7p1a4FFh_mKU1HLbI,4226
34
40
  pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
35
- pykubegrader/widgets_base/select.py,sha256=uMncmVIqjvJkffMQY1L_PokrFCidK1PeVITX0i70fho,2750
36
- PyKubeGrader-0.3.7.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
37
- PyKubeGrader-0.3.7.dist-info/METADATA,sha256=Ht8G9VvFfGBa9YD9PoBSbd_Cee6tuGZ2RTBjFN0Z5pk,2729
38
- PyKubeGrader-0.3.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
- PyKubeGrader-0.3.7.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
40
- PyKubeGrader-0.3.7.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
41
- PyKubeGrader-0.3.7.dist-info/RECORD,,
41
+ pykubegrader/widgets_base/select.py,sha256=qP31bjTWIn8LEgKwtNUJbgJnum6P7bx6A_At-u1ivFk,2750
42
+ PyKubeGrader-0.3.9.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
43
+ PyKubeGrader-0.3.9.dist-info/METADATA,sha256=fPAObIviDbz7vOFs5JCQm_IBTI1lT8W_l2TRIqhkPhM,2757
44
+ PyKubeGrader-0.3.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
45
+ PyKubeGrader-0.3.9.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
46
+ PyKubeGrader-0.3.9.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
47
+ PyKubeGrader-0.3.9.dist-info/RECORD,,
@@ -1,11 +1,11 @@
1
1
  import ast
2
+ import base64
2
3
  import json
3
4
  import re
4
5
  import shutil
6
+ import sys
5
7
  from dataclasses import dataclass
6
8
  from pathlib import Path
7
- from typing import Optional
8
- import base64
9
9
  from typing import Any, Optional
10
10
 
11
11
  import nbformat
@@ -15,17 +15,17 @@ import nbformat
15
15
  class FastAPINotebookBuilder:
16
16
  notebook_path: str
17
17
  temp_notebook: Optional[str] = None
18
- assignment_tag: Optional[str] = ""
19
- require_key: Optional[bool] = False
20
- verbose: Optional[bool] = False
18
+ assignment_tag: str = ""
19
+ require_key: bool = False
20
+ verbose: bool = False
21
21
 
22
22
  def __post_init__(self) -> None:
23
23
  self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
24
24
  self.notebook_path
25
25
  )
26
- self.total_points = 0
26
+ self.total_points = 0.0
27
27
 
28
- self.max_question_points = {}
28
+ self.max_question_points: dict[str, float] = {}
29
29
  self.run()
30
30
 
31
31
  def run(self) -> None:
@@ -87,20 +87,24 @@ class FastAPINotebookBuilder:
87
87
  for i, question in enumerate(self.max_question_points.keys()):
88
88
  index, source = self.find_question_description(question)
89
89
  try:
90
- modified_source = FastAPINotebookBuilder.add_text_after_double_hash(source, f"Question {i+1} (Points: {self.max_question_points[question]}):")
90
+ modified_source = FastAPINotebookBuilder.add_text_after_double_hash(
91
+ source,
92
+ f"Question {i + 1} (Points: {self.max_question_points[question]}):",
93
+ )
91
94
  self.replace_cell_source(index, modified_source)
92
- except:
95
+ except Exception as e:
96
+ print(f"Error adding question description: {e}", file=sys.stderr)
93
97
  pass
94
98
 
95
99
  for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
96
- if self.verbose:
100
+ if self.verbose:
97
101
  print(
98
102
  f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
99
103
  )
100
104
 
101
105
  cell = self.get_cell(cell_index)
102
106
  cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
103
- cell["source"], require_key=self.require_key,
107
+ cell["source"], require_key=self.require_key, assignment_tag = self.assignment_tag,
104
108
  )
105
109
 
106
110
  cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
@@ -146,41 +150,47 @@ class FastAPINotebookBuilder:
146
150
  FastAPINotebookBuilder.construct_update_responses(cell_dict)
147
151
  )
148
152
 
149
- self.replace_cell_source(cell_index, updated_cell_source)
153
+ self.replace_cell_source(cell_index, updated_cell_source)
150
154
 
151
155
  def find_question_description(self, search_string):
152
- with open(self.temp_notebook, 'r', encoding='utf-8') as f:
156
+ with open(self.temp_notebook, "r", encoding="utf-8") as f:
153
157
  nb_data = json.load(f)
154
158
 
155
159
  found_raw = False
156
160
 
157
161
  for idx, cell in enumerate(nb_data.get("cells", [])):
158
- if cell["cell_type"] == "raw" and any("# BEGIN QUESTION" in line for line in cell.get("source", [])) and any(search_string in line for line in cell.get("source", [])):
162
+ if (
163
+ cell["cell_type"] == "raw"
164
+ and any("# BEGIN QUESTION" in line for line in cell.get("source", []))
165
+ and any(search_string in line for line in cell.get("source", []))
166
+ ):
159
167
  found_raw = True
160
168
  elif found_raw and cell["cell_type"] == "markdown":
161
- return idx, cell.get("source", []) # Return the index of the first matching markdown cell
169
+ return idx, cell.get(
170
+ "source", []
171
+ ) # Return the index of the first matching markdown cell
162
172
 
163
- return None, None # Return None if no such markdown cell is found
173
+ return None, None # Return None if no such markdown cell is found
164
174
 
165
175
  def add_total_points_to_notebook(self) -> None:
166
176
  self.max_question_points.keys()
167
177
 
168
178
  def get_max_question_points(self, cell_dict) -> float:
169
179
  return sum(
170
- cell["points"]
171
- for cell in self.assertion_tests_dict.values()
172
- if cell["question"] == cell_dict["question"]
173
- )
180
+ cell["points"]
181
+ for cell in self.assertion_tests_dict.values()
182
+ if cell["question"] == cell_dict["question"]
183
+ )
174
184
 
175
185
  @staticmethod
176
186
  def add_text_after_double_hash(markdown_source, insert_text):
177
187
  """
178
188
  Adds insert_text immediately after the first '##' in the first line that starts with '##'.
179
-
189
+
180
190
  Args:
181
191
  - markdown_source (list of str): The list of lines in the markdown cell.
182
192
  - insert_text (str): The text to be inserted.
183
-
193
+
184
194
  Returns:
185
195
  - list of str: The modified markdown cell content.
186
196
  """
@@ -189,7 +199,9 @@ class FastAPINotebookBuilder:
189
199
 
190
200
  for line in markdown_source:
191
201
  if not inserted and line.startswith("## "):
192
- modified_source.append(f"## {insert_text} {line[3:]}") # Insert text after '##'
202
+ modified_source.append(
203
+ f"## {insert_text} {line[3:]}"
204
+ ) # Insert text after '##'
193
205
  inserted = True # Ensure it only happens once
194
206
  else:
195
207
  modified_source.append(line)
@@ -204,7 +216,9 @@ class FastAPINotebookBuilder:
204
216
  max_question_points = self.get_max_question_points(cell_dict)
205
217
 
206
218
  # store the max points for the question
207
- self.max_question_points[f"{cell_dict["question"]}"] = max_question_points
219
+ self.max_question_points[f"{cell_dict['question']}"] = (
220
+ max_question_points
221
+ )
208
222
 
209
223
  self.total_points += max_question_points
210
224
 
@@ -224,7 +238,7 @@ class FastAPINotebookBuilder:
224
238
 
225
239
  if self.require_key:
226
240
  first_cell_header.append(
227
- "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
241
+ f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment='{self.assignment_tag}')\n"
228
242
  )
229
243
 
230
244
  short_filename = self.filename.split(".")[0].replace("_temp", "")
@@ -330,7 +344,9 @@ class FastAPINotebookBuilder:
330
344
  return original_list[:index] + insert_list + original_list[index:]
331
345
 
332
346
  @staticmethod
333
- def add_import_statements_to_tests(cell_source: list[str], require_key:bool = False) -> list[str]:
347
+ def add_import_statements_to_tests(
348
+ cell_source: list[str], require_key: bool = False
349
+ ) -> list[str]:
334
350
  """
335
351
  Adds the necessary import statements to the first cell of the notebook.
336
352
  """
@@ -353,7 +369,7 @@ class FastAPINotebookBuilder:
353
369
 
354
370
  if require_key:
355
371
  imports.append(
356
- "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
372
+ f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment='{assignment_tag}')\n"
357
373
  )
358
374
 
359
375
  for i, line in enumerate(cell_source):
@@ -83,7 +83,7 @@ class NotebookProcessor:
83
83
  self.require_key = assignment.get("require_key", False)
84
84
  self.assignment_tag = assignment.get(
85
85
  "assignment_tag",
86
- f"week{assignment.get("week")}-{self.assignment_type}",
86
+ f"week{assignment.get('week')}-{self.assignment_type}",
87
87
  )
88
88
  else:
89
89
  self.assignment_type = self.assignment_tag.split("-")[0].lower()
@@ -578,7 +578,7 @@ class NotebookProcessor:
578
578
 
579
579
  if self.require_key:
580
580
  # Add an additional line for validate_token()
581
- validate_token_line =f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
581
+ validate_token_line = f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
582
582
 
583
583
  # Define the Code cell
584
584
  code_cell = nbformat.v4.new_code_cell(
@@ -681,7 +681,7 @@ class NotebookProcessor:
681
681
  self.week,
682
682
  self.assignment_type,
683
683
  require_key=self.require_key,
684
- assignment_tag = self.assignment_tag,
684
+ assignment_tag=self.assignment_tag,
685
685
  )
686
686
 
687
687
  NotebookProcessor.replace_temp_in_notebook(
@@ -712,7 +712,7 @@ class NotebookProcessor:
712
712
  self.week,
713
713
  self.assignment_type,
714
714
  require_key=self.require_key,
715
- assignment_tag = self.assignment_tag
715
+ assignment_tag=self.assignment_tag,
716
716
  )
717
717
  NotebookProcessor.replace_temp_no_otter(
718
718
  temp_notebook_path, temp_notebook_path
@@ -744,7 +744,9 @@ class NotebookProcessor:
744
744
  nbformat.write(notebook, f)
745
745
 
746
746
  @staticmethod
747
- def add_validate_token_cell(notebook_path: str, require_key: bool, **kwargs) -> None:
747
+ def add_validate_token_cell(
748
+ notebook_path: str, require_key: bool, **kwargs
749
+ ) -> None:
748
750
  """
749
751
  Adds a new code cell at the top of a Jupyter notebook if require_key is True.
750
752
 
@@ -758,8 +760,12 @@ class NotebookProcessor:
758
760
  if not require_key:
759
761
  print("require_key is False. No changes made to the notebook.")
760
762
  return
761
-
762
- NotebookProcessor.add_validate_block(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
763
+
764
+ NotebookProcessor.add_validate_block(
765
+ notebook_path,
766
+ require_key,
767
+ assignment_tag=kwargs.get("assignment_tag", None),
768
+ )
763
769
 
764
770
  # Load the notebook
765
771
  with open(notebook_path, "r", encoding="utf-8") as f:
@@ -768,13 +774,13 @@ class NotebookProcessor:
768
774
  # Create the new code cell
769
775
  if kwargs.get("assignment_tag", None):
770
776
  new_cell = nbformat.v4.new_code_cell(
771
- "from pykubegrader.tokens.validate_token import validate_token\n"
772
- f"validate_token('type the key provided by your instructor here', assignment = '{kwargs.get('assignment_tag')}')\n"
777
+ "from pykubegrader.tokens.validate_token import validate_token\n"
778
+ f"validate_token('type the key provided by your instructor here', assignment = '{kwargs.get('assignment_tag')}')\n"
773
779
  )
774
780
  else:
775
781
  new_cell = nbformat.v4.new_code_cell(
776
- "from pykubegrader.tokens.validate_token import validate_token\n"
777
- "validate_token('type the key provided by your instructor here')\n"
782
+ "from pykubegrader.tokens.validate_token import validate_token\n"
783
+ "validate_token('type the key provided by your instructor here')\n"
778
784
  )
779
785
 
780
786
  # Add the new cell to the top of the notebook
@@ -785,7 +791,7 @@ class NotebookProcessor:
785
791
  nbformat.write(notebook, f)
786
792
 
787
793
  @staticmethod
788
- def add_validate_block(notebook_path: str, require_key: bool, **kwargs) -> None:
794
+ def add_validate_block(notebook_path: str, require_key: bool, assignment_tag = None, **kwargs) -> None:
789
795
  """
790
796
  Modifies the first code cell of a Jupyter notebook to add the validate_token call if require_key is True.
791
797
 
@@ -804,7 +810,7 @@ class NotebookProcessor:
804
810
  notebook = nbformat.read(f, as_version=4)
805
811
 
806
812
  # Prepare the validation code
807
- validation_code = "validate_token()\n"
813
+ validation_code = f"validate_token(assignment = '{assignment_tag}')\n"
808
814
 
809
815
  # Modify the first cell if it's a code cell, otherwise insert a new one
810
816
  if notebook.cells and notebook.cells[0].cell_type == "code":
@@ -819,7 +825,11 @@ class NotebookProcessor:
819
825
 
820
826
  @staticmethod
821
827
  def add_initialization_code(
822
- notebook_path, week, assignment_type, require_key=False, **kwargs,
828
+ notebook_path,
829
+ week,
830
+ assignment_type,
831
+ require_key=False,
832
+ **kwargs,
823
833
  ):
824
834
  # finds the first code cell
825
835
  index, cell = find_first_code_cell(notebook_path)
@@ -831,7 +841,11 @@ class NotebookProcessor:
831
841
  replace_cell_source(notebook_path, index, cell)
832
842
 
833
843
  if require_key:
834
- NotebookProcessor.add_validate_token_cell(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
844
+ NotebookProcessor.add_validate_token_cell(
845
+ notebook_path,
846
+ require_key,
847
+ assignment_tag=kwargs.get("assignment_tag", None),
848
+ )
835
849
 
836
850
  def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
837
851
  ### Parse the notebook for multiple choice questions
@@ -0,0 +1,184 @@
1
+ import numpy as np
2
+ from dateutil import parser
3
+ from datetime import datetime
4
+ from pykubegrader.graders.late_assignments import calculate_late_submission
5
+
6
+
7
+ class assignment_type:
8
+ """
9
+ Base class for assignment types.
10
+
11
+ Attributes:
12
+ weight (float): The weight of the assignment in the overall grade.
13
+
14
+ Methods:
15
+ __init__(name: str, weekly: bool, weight: float):
16
+ Initializes an instance of the assignment_type class.
17
+ """
18
+
19
+ def __init__(self, name: str, weekly: bool, weight: float):
20
+ """Initializes an instance of the assignment_type class.
21
+ Args:
22
+ name (str): The name of the assignment.
23
+ weekly (bool): Indicates if the assignment is weekly.
24
+ weight (float): The weight of the assignment in the overall grade."""
25
+ self.name = name
26
+ self.weekly = weekly
27
+ self.weight = weight
28
+
29
+
30
+ class Assignment(assignment_type):
31
+ """
32
+ Class for storing and updating assignment scores.
33
+
34
+ Attributes:
35
+ week (int, optional): The week number of the assignment.
36
+ exempted (bool): Indicates if the assignment is exempted.
37
+ graded (bool): Indicates if the assignment has been graded.
38
+ late_adjustment (bool): Indicates if late submissions are allowed.
39
+ students_exempted (list): List of student IDs exempted from the assignment.
40
+ due_date (datetime, optional): The due date of the assignment.
41
+ max_score (float, optional): The maximum score possible for the assignment.
42
+ grade_adjustment_func (callable, optional): Function to adjust the grade for late or exempted submissions.
43
+
44
+ Methods:
45
+ add_exempted_students(students):
46
+ Add students to the exempted list.
47
+
48
+ update_score(submission=None):
49
+ Update the score of the assignment based on the submission.
50
+
51
+ grade_adjustment(submission):
52
+ Apply the adjustment function if provided.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ name: str,
58
+ weekly: bool,
59
+ weight: float,
60
+ score: float,
61
+ grade_adjustment_func=None,
62
+ **kwargs,
63
+ ):
64
+ """
65
+ Initializes an instance of the Assignment class.
66
+
67
+ weekly (bool): Indicates if the assignment is weekly.
68
+ grade_adjustment_func (callable, optional): Used to calculate the grade in the case of late or exempted submissions. Defaults to None.
69
+ **kwargs: Additional keyword arguments.
70
+ week (int, optional): The week number of the assignment. Defaults to None.
71
+ exempted (bool, optional): Indicates if the assignment is exempted. Defaults to False.
72
+ graded (bool, optional): Indicates if the assignment is graded. Defaults to False.
73
+ late_adjustment (bool, optional): Indicates if late adjustment is applied. Defaults to True.
74
+ students_exempted (list, optional): List of students exempted from the assignment. Defaults to an empty list.
75
+ due_date (datetime, optional): The due date of the assignment. Defaults to None.
76
+ max_score (float, optional): The maximum score possible for the assignment. Defaults to None.
77
+ """
78
+ super().__init__(name, weekly, weight)
79
+ self.score = score
80
+ self._score = score
81
+ self.week = kwargs.get("week", None)
82
+ self.exempted = kwargs.get("exempted", False)
83
+ self.graded = kwargs.get("graded", False)
84
+ self.late_adjustment = kwargs.get("late_adjustment", True)
85
+ self.students_exempted = kwargs.get("students_exempted", [])
86
+ self.due_date = kwargs.get("due_date", None)
87
+ self.max_score = kwargs.get("max_score", None)
88
+
89
+ # Store the function for later use
90
+ self.grade_adjustment_func = grade_adjustment_func
91
+
92
+ def add_exempted_students(self, students):
93
+ """
94
+ Add students to the exempted list.
95
+ Args:
96
+ students (list): List of student IDs to exempt from the assignment.
97
+ """
98
+ self.students_exempted.extend(students)
99
+
100
+ def update_score(self, submission=None):
101
+ """Updates the assignment score based on the given submission.
102
+
103
+ This method adjusts the score using the `grade_adjustment` function if a submission
104
+ is provided. If the submission results in a higher score than the current score,
105
+ the assignment score is updated. If no submission is provided and the student is
106
+ not exempted, the score is set to zero. If the student is exempted, the score
107
+ is set to NaN.
108
+
109
+ Args:
110
+ submission (dict, optional): The submission data, expected to contain relevant
111
+ details for grading. Defaults to None.
112
+
113
+ Returns:
114
+ float: The updated assignment score. If exempted, returns NaN. If no submission
115
+ is provided, returns 0.
116
+ """
117
+ if self.exempted:
118
+ self.score = np.nan
119
+
120
+ # Saves a table with the score of the exempted assignment still recorded.
121
+ try:
122
+ # Adjust the score based on submission
123
+ score_ = self.grade_adjustment(submission)
124
+ if score_ > self._score:
125
+ self._score = score_
126
+ except:
127
+ pass
128
+ return self.score
129
+ elif submission is not None:
130
+ # Adjust the score based on submission
131
+ score_ = self.grade_adjustment(submission)
132
+
133
+ # Update the score only if the new score is higher
134
+ if score_ > self.score:
135
+ self.score = score_
136
+ self._score = score_
137
+
138
+ return self.score
139
+ else:
140
+ # Set the score to zero if not exempted and no submission
141
+ self.score = 0
142
+ self._score = 0
143
+ return self.score
144
+
145
+ def grade_adjustment(self, submission):
146
+ """Applies adjustments to the submission score based on grading policies.
147
+
148
+ This method applies any provided grade adjustment function to the raw score.
149
+ If no custom function is given, it determines the final score by considering
150
+ lateness penalties based on the submission timestamp and due date.
151
+
152
+ Args:
153
+ submission (dict): A dictionary containing:
154
+ - `"raw_score"` (float): The initial unadjusted score.
155
+ - `"timestamp"` (str): The submission timestamp in a parsable format.
156
+
157
+ Returns:
158
+ float: The adjusted score, incorporating lateness penalties if applicable.
159
+ Returns 0 for late submissions if no late adjustment policy is defined.
160
+ """
161
+ score = submission["raw_score"]
162
+ entry_date = parser.parse(submission["timestamp"])
163
+
164
+ if self.grade_adjustment_func:
165
+ return self.grade_adjustment_func(score)
166
+ else:
167
+ if self.late_adjustment:
168
+ # Convert due date to datetime object
169
+ due_date = datetime.fromisoformat(self.due_date.replace("Z", "+00:00"))
170
+
171
+ late_modifier = calculate_late_submission(
172
+ due_date.strftime("%Y-%m-%d %H:%M:%S"),
173
+ entry_date.strftime("%Y-%m-%d %H:%M:%S"),
174
+ )
175
+
176
+ # Apply late modifier and normalize score
177
+ return (score / self.max_score) * late_modifier
178
+ else:
179
+ # Return normalized score if on time
180
+ if entry_date < self.due_date:
181
+ return score / self.max_score
182
+ # Assign zero score for late submissions without a late adjustment policy
183
+ else:
184
+ return 0