PyKubeGrader 0.3.4__py3-none-any.whl → 0.3.6__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.4
3
+ Version: 0.3.6
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,12 +1,12 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
2
  pykubegrader/grading_tester.ipynb,sha256=wwT9jyhpR6GGM8r4todaGfrsUxS6JxM0qIqMcDYKM7w,18839
3
3
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
4
- pykubegrader/telemetry.py,sha256=ooLK-dY_hJQ7t4r83hWyO8wx6F_7TfWJS7tCp_nH7r8,13049
4
+ pykubegrader/telemetry.py,sha256=vZK9p3XqnqacwtiVyZgjI2mcIr5ZcxRRwW5sAZsrJkE,16631
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=dlcVrGgsvxnt6GlAUN3e-FrpsPNJKXSHni1fstRCBik,20311
9
- pykubegrader/build/build_folder.py,sha256=Asc-VdhXgxQfOfFIWJShhXrF2EITJOIZQ5Dz_2y-P2I,85358
8
+ pykubegrader/build/api_notebook_builder.py,sha256=EZG4Ow4YATzOWPPNLkdQEdWt7hkpbaI5ZD1Bf2KEWeY,25622
9
+ pykubegrader/build/build_folder.py,sha256=9aewc4dcYKS5kNaDS3gTwCnzRTmHT6Ooio5TF258q_8,86963
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
@@ -17,8 +17,9 @@ pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YB
17
17
  pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
18
18
  pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
19
19
  pykubegrader/submit/submit_assignment.py,sha256=cqVu7US8GVaCdJdaU2yjawlVBtAKP5XJc4oAvX5FeRU,2575
20
- pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
21
- pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
20
+ pykubegrader/tokens/token_panel.py,sha256=NNA5ZV3Q9jB_lz2aSwMyViXV0ESu6V_7T92Qji7UpSQ,1377
21
+ pykubegrader/tokens/tokens.py,sha256=qcYMFgNPimbfeS7lXOtbgquGgeJCgOGx5hvXewIs0oQ,1474
22
+ pykubegrader/tokens/validate_token.py,sha256=kvHX0NJBm21xzb2p67j7vq1La6J1XbmobEJQ3fTMdZA,3289
22
23
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
24
  pykubegrader/widgets/multiple_choice.py,sha256=ag6W-HN7isHkIUmB4BxtK8T1JhuV3FBLUBAhcV6rN80,2729
24
25
  pykubegrader/widgets/question_processor.py,sha256=fFH2ffMPYAJHsDn1RweEBnibfoZlSvTANUxYT3EPb5w,1375
@@ -29,12 +30,12 @@ pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs
29
30
  pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
30
31
  pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
31
32
  pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
32
- pykubegrader/widgets_base/multi_select.py,sha256=JgjhHQJL8Pf0-1T_wdZCecAK1IgVJrZBCbR6b3jvDtk,4181
33
+ pykubegrader/widgets_base/multi_select.py,sha256=KtfAP0PyEbcjWlKNpI5_5-PLMtcUbbNX0Es_-w-H34Q,4226
33
34
  pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
34
- pykubegrader/widgets_base/select.py,sha256=tEDg7GEjsZnz1646YTthTeamujVRS5jDJWMsXhmOQbI,2705
35
- PyKubeGrader-0.3.4.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
36
- PyKubeGrader-0.3.4.dist-info/METADATA,sha256=6aq3PWnDPR8lNxPMmWvDkbd2GvZNfLkpYNcpOLSbHqc,2729
37
- PyKubeGrader-0.3.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
38
- PyKubeGrader-0.3.4.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
39
- PyKubeGrader-0.3.4.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
40
- PyKubeGrader-0.3.4.dist-info/RECORD,,
35
+ pykubegrader/widgets_base/select.py,sha256=uMncmVIqjvJkffMQY1L_PokrFCidK1PeVITX0i70fho,2750
36
+ PyKubeGrader-0.3.6.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
37
+ PyKubeGrader-0.3.6.dist-info/METADATA,sha256=RVMHbbz9895jEBhpogU7TM5keqyf-kUKhKJhJ1Hk168,2729
38
+ PyKubeGrader-0.3.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
+ PyKubeGrader-0.3.6.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
40
+ PyKubeGrader-0.3.6.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
41
+ PyKubeGrader-0.3.6.dist-info/RECORD,,
@@ -5,6 +5,8 @@ import shutil
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Optional
8
+ import base64
9
+ from typing import Any, Optional
8
10
 
9
11
  import nbformat
10
12
 
@@ -15,15 +17,18 @@ class FastAPINotebookBuilder:
15
17
  temp_notebook: Optional[str] = None
16
18
  assignment_tag: Optional[str] = ""
17
19
  require_key: Optional[bool] = False
20
+ verbose: Optional[bool] = False
18
21
 
19
- def __post_init__(self):
22
+ def __post_init__(self) -> None:
20
23
  self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
21
24
  self.notebook_path
22
25
  )
23
26
  self.total_points = 0
27
+
28
+ self.max_question_points = {}
24
29
  self.run()
25
30
 
26
- def run(self):
31
+ def run(self) -> None:
27
32
  # here for easy debugging
28
33
  if self.temp_notebook is not None:
29
34
  shutil.copy(
@@ -36,25 +41,74 @@ class FastAPINotebookBuilder:
36
41
  self.assertion_tests_dict = self.question_dict()
37
42
  self.add_api_code()
38
43
 
39
- def add_api_code(self):
44
+ # add the point total to the end of the notebook
45
+ self.add_total_points_to_notebook()
46
+
47
+ @staticmethod
48
+ def conceal_tests(cell_source):
49
+ """
50
+ Takes a list of code lines, detects blocks between `# BEGIN HIDE` and `# END HIDE`,
51
+ encodes them in Base64, and replaces them with an `exec()` statement.
52
+
53
+ Returns a new list of lines with the concealed blocks.
54
+ """
55
+
56
+ concealed_lines = []
57
+ hide_mode = False
58
+ hidden_code = []
59
+
60
+ for line in cell_source:
61
+ if "# BEGIN HIDE" in line:
62
+ hide_mode = True
63
+ hidden_code = [] # Start a new hidden block
64
+ concealed_lines.append(line) # Keep the marker for clarity
65
+ continue
66
+ elif "# END HIDE" in line:
67
+ hide_mode = False
68
+ # Encode the entire block
69
+ encoded_block = base64.b64encode(
70
+ "\n".join(hidden_code).encode()
71
+ ).decode()
72
+ concealed_lines.append(
73
+ f'exec(base64.b64decode("{encoded_block}").decode()) # Obfuscated\n'
74
+ )
75
+ concealed_lines.append(line) # Keep the marker for clarity
76
+ continue
77
+
78
+ if hide_mode:
79
+ hidden_code.append(line.strip()) # Collect hidden code
80
+ else:
81
+ concealed_lines.append(line)
82
+
83
+ return concealed_lines
84
+
85
+ def add_api_code(self) -> None:
40
86
  self.compute_max_points_free_response()
87
+ for i, question in enumerate(self.max_question_points.keys()):
88
+ index, source = self.find_question_description(question)
89
+ try:
90
+ modified_source = FastAPINotebookBuilder.add_text_after_double_hash(source, f"Question {i+1} (Points: {self.max_question_points[question]}):")
91
+ self.replace_cell_source(index, modified_source)
92
+ except:
93
+ pass
41
94
 
42
95
  for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
43
- print(
44
- f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
45
- )
96
+ if self.verbose:
97
+ print(
98
+ f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
99
+ )
46
100
 
47
101
  cell = self.get_cell(cell_index)
48
102
  cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
49
- cell["source"]
103
+ cell["source"], require_key=self.require_key,
50
104
  )
51
105
 
106
+ cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
107
+
52
108
  last_import_line_ind = FastAPINotebookBuilder.find_last_import_line(
53
109
  cell_source
54
110
  )
55
111
 
56
- # header, body = FastAPINotebookBuilder.split_list_at_marker(cell_source)
57
-
58
112
  updated_cell_source = []
59
113
  updated_cell_source.extend(cell_source[: last_import_line_ind + 1])
60
114
  if cell_dict["is_first"]:
@@ -92,22 +146,69 @@ class FastAPINotebookBuilder:
92
146
  FastAPINotebookBuilder.construct_update_responses(cell_dict)
93
147
  )
94
148
 
95
- self.replace_cell_source(cell_index, updated_cell_source)
149
+ self.replace_cell_source(cell_index, updated_cell_source)
96
150
 
97
- def compute_max_points_free_response(self):
98
- for cell_dict in self.assertion_tests_dict.values():
99
- # gets the question name from the first cell to not double count
100
- if cell_dict["is_first"]:
101
- # get the max points for the question
102
- max_question_points = sum(
151
+ def find_question_description(self, search_string):
152
+ with open(self.temp_notebook, 'r', encoding='utf-8') as f:
153
+ nb_data = json.load(f)
154
+
155
+ found_raw = False
156
+
157
+ 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", [])):
159
+ found_raw = True
160
+ elif found_raw and cell["cell_type"] == "markdown":
161
+ return idx, cell.get("source", []) # Return the index of the first matching markdown cell
162
+
163
+ return None, None # Return None if no such markdown cell is found
164
+
165
+ def add_total_points_to_notebook(self) -> None:
166
+ self.max_question_points.keys()
167
+
168
+ def get_max_question_points(self, cell_dict) -> float:
169
+ return sum(
103
170
  cell["points"]
104
171
  for cell in self.assertion_tests_dict.values()
105
172
  if cell["question"] == cell_dict["question"]
106
173
  )
107
174
 
175
+ @staticmethod
176
+ def add_text_after_double_hash(markdown_source, insert_text):
177
+ """
178
+ Adds insert_text immediately after the first '##' in the first line that starts with '##'.
179
+
180
+ Args:
181
+ - markdown_source (list of str): The list of lines in the markdown cell.
182
+ - insert_text (str): The text to be inserted.
183
+
184
+ Returns:
185
+ - list of str: The modified markdown cell content.
186
+ """
187
+ modified_source = []
188
+ inserted = False
189
+
190
+ for line in markdown_source:
191
+ if not inserted and line.startswith("## "):
192
+ modified_source.append(f"## {insert_text} {line[3:]}") # Insert text after '##'
193
+ inserted = True # Ensure it only happens once
194
+ else:
195
+ modified_source.append(line)
196
+
197
+ return modified_source
198
+
199
+ def compute_max_points_free_response(self) -> None:
200
+ for cell_dict in self.assertion_tests_dict.values():
201
+ # gets the question name from the first cell to not double count
202
+ if cell_dict["is_first"]:
203
+ # get the max points for the question
204
+ max_question_points = self.get_max_question_points(cell_dict)
205
+
206
+ # store the max points for the question
207
+ self.max_question_points[f"{cell_dict["question"]}"] = max_question_points
208
+
108
209
  self.total_points += max_question_points
109
210
 
110
- def construct_first_cell_question_header(self, cell_dict):
211
+ def construct_first_cell_question_header(self, cell_dict: dict) -> list[str]:
111
212
  max_question_points = sum(
112
213
  cell["points"]
113
214
  for cell in self.assertion_tests_dict.values()
@@ -136,7 +237,7 @@ class FastAPINotebookBuilder:
136
237
  return first_cell_header
137
238
 
138
239
  @staticmethod
139
- def construct_update_responses(cell_dict):
240
+ def construct_update_responses(cell_dict: dict) -> list[str]:
140
241
  update_responses = []
141
242
 
142
243
  logging_variables = cell_dict["logging_variables"]
@@ -149,7 +250,9 @@ class FastAPINotebookBuilder:
149
250
  return update_responses
150
251
 
151
252
  @staticmethod
152
- def split_list_at_marker(input_list, marker="""# END TEST CONFIG"""):
253
+ def split_list_at_marker(
254
+ input_list: list[str], marker: str = """# END TEST CONFIG"""
255
+ ) -> tuple[list[str], list[str]]:
153
256
  """
154
257
  Splits a list into two parts at the specified marker string.
155
258
 
@@ -172,7 +275,7 @@ class FastAPINotebookBuilder:
172
275
  ) # If the marker is not in the list, return the original list and an empty list
173
276
 
174
277
  @staticmethod
175
- def construct_graders(cell_dict):
278
+ def construct_graders(cell_dict: dict) -> list[str]:
176
279
  # Generate Python code
177
280
  added_code = [
178
281
  "if "
@@ -184,7 +287,7 @@ class FastAPINotebookBuilder:
184
287
  return added_code
185
288
 
186
289
  @staticmethod
187
- def construct_question_info(cell_dict):
290
+ def construct_question_info(cell_dict: dict) -> list[str]:
188
291
  question_info = []
189
292
 
190
293
  question_id = cell_dict["question"] + "-" + str(cell_dict["test_number"])
@@ -197,8 +300,12 @@ class FastAPINotebookBuilder:
197
300
 
198
301
  @staticmethod
199
302
  def insert_list_at_index(
200
- original_list, insert_list, index, line_break=True, inplace_line_break=True
201
- ):
303
+ original_list: list[str],
304
+ insert_list: list[str],
305
+ index: int,
306
+ line_break: bool = True,
307
+ inplace_line_break: bool = True,
308
+ ) -> list[str]:
202
309
  """
203
310
  Inserts a list into another list at a specific index.
204
311
 
@@ -223,7 +330,7 @@ class FastAPINotebookBuilder:
223
330
  return original_list[:index] + insert_list + original_list[index:]
224
331
 
225
332
  @staticmethod
226
- def add_import_statements_to_tests(cell_source):
333
+ def add_import_statements_to_tests(cell_source: list[str], require_key:bool = False) -> list[str]:
227
334
  """
228
335
  Adds the necessary import statements to the first cell of the notebook.
229
336
  """
@@ -241,8 +348,14 @@ class FastAPINotebookBuilder:
241
348
  " update_responses,\n",
242
349
  ")\n",
243
350
  "import os\n",
351
+ "import base64\n",
244
352
  ]
245
353
 
354
+ if require_key:
355
+ imports.append(
356
+ "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
357
+ )
358
+
246
359
  for i, line in enumerate(cell_source):
247
360
  if end_test_config_line in line:
248
361
  # Insert the imports immediately after the current line
@@ -251,7 +364,12 @@ class FastAPINotebookBuilder:
251
364
  ] + imports # Add a blank line for readability
252
365
  return cell_source # Exit the loop once the imports are inserted
253
366
 
254
- def extract_first_cell(self):
367
+ raise ValueError("End of test configuration not found")
368
+
369
+ # TODO: `Any` return not good; would be better to specify return type(s)
370
+ def extract_first_cell(self) -> Any:
371
+ if not self.temp_notebook:
372
+ raise ValueError("No temporary notebook file path provided")
255
373
  with open(self.temp_notebook, "r", encoding="utf-8") as f:
256
374
  notebook = json.load(f)
257
375
  if "cells" in notebook and len(notebook["cells"]) > 0:
@@ -260,13 +378,16 @@ class FastAPINotebookBuilder:
260
378
  return None
261
379
 
262
380
  @staticmethod
263
- def get_filename_and_root(path):
381
+ def get_filename_and_root(path: str) -> tuple[Path, str]:
264
382
  path_obj = Path(path).resolve() # Resolve the path to get an absolute path
265
383
  root_path = path_obj.parent # Get the parent directory
266
384
  filename = path_obj.name # Get the filename
267
385
  return root_path, filename
268
386
 
269
- def get_cell(self, cell_index):
387
+ # TODO: `Any` return not good; would be better to specify return type(s)
388
+ def get_cell(self, cell_index: int) -> Any:
389
+ if not self.temp_notebook:
390
+ raise ValueError("No temporary notebook file path provided")
270
391
  with open(self.temp_notebook, "r", encoding="utf-8") as f:
271
392
  notebook = json.load(f)
272
393
  if "cells" in notebook and len(notebook["cells"]) > cell_index:
@@ -274,7 +395,7 @@ class FastAPINotebookBuilder:
274
395
  else:
275
396
  return None
276
397
 
277
- def replace_cell_source(self, cell_index, new_source):
398
+ def replace_cell_source(self, cell_index: int, new_source: str | list[str]) -> None:
278
399
  """
279
400
  Replace the source code of a specific Jupyter notebook cell.
280
401
 
@@ -283,6 +404,8 @@ class FastAPINotebookBuilder:
283
404
  new_source (str): New source code to replace the cell's content.
284
405
  """
285
406
  # Load the notebook
407
+ if not self.temp_notebook:
408
+ raise ValueError("No temporary notebook file path provided")
286
409
  with open(self.temp_notebook, "r", encoding="utf-8") as f:
287
410
  notebook = nbformat.read(f, as_version=4)
288
411
 
@@ -301,7 +424,7 @@ class FastAPINotebookBuilder:
301
424
  print(f"Updated notebook saved to {self.temp_notebook}")
302
425
 
303
426
  @staticmethod
304
- def find_last_import_line(cell_source):
427
+ def find_last_import_line(cell_source: list[str]) -> int:
305
428
  """
306
429
  Finds the index of the last line with an import statement in a list of code lines,
307
430
  including multiline import statements.
@@ -339,7 +462,7 @@ class FastAPINotebookBuilder:
339
462
  return last_import_index
340
463
 
341
464
  @staticmethod
342
- def extract_log_variables(cell):
465
+ def extract_log_variables(cell: dict) -> list[str]:
343
466
  """Extracts log variables from the first cell."""
344
467
  if "source" in cell:
345
468
  for line in cell["source"]:
@@ -355,7 +478,8 @@ class FastAPINotebookBuilder:
355
478
  pass
356
479
  return []
357
480
 
358
- def tag_questions(cells_dict):
481
+ @staticmethod
482
+ def tag_questions(cells_dict: dict) -> dict:
359
483
  """
360
484
  Adds 'is_first' and 'is_last' boolean flags to the cells based on their position
361
485
  within the group of the same question. All cells will have both flags.
@@ -377,7 +501,7 @@ class FastAPINotebookBuilder:
377
501
  raise KeyError(f"Cell {key} is missing the 'question' key.")
378
502
 
379
503
  # Group the keys by question name
380
- question_groups = {}
504
+ question_groups: dict = {}
381
505
  for key, cell in cells_dict.items():
382
506
  question = cell.get(
383
507
  "question"
@@ -397,7 +521,9 @@ class FastAPINotebookBuilder:
397
521
 
398
522
  return cells_dict
399
523
 
400
- def question_dict(self):
524
+ def question_dict(self) -> dict:
525
+ if not self.temp_notebook:
526
+ raise ValueError("No temporary notebook file path provided")
401
527
  notebook_path = Path(self.temp_notebook)
402
528
  if not notebook_path.exists():
403
529
  raise FileNotFoundError(f"The file {notebook_path} does not exist.")
@@ -406,15 +532,14 @@ class FastAPINotebookBuilder:
406
532
  notebook = json.load(f)
407
533
 
408
534
  results_dict = {}
535
+ question_name = None # At least define the variable up front
409
536
 
410
537
  for cell_index, cell in enumerate(notebook.get("cells", [])):
411
538
  if cell.get("cell_type") == "raw":
412
539
  source = "".join(cell.get("source", ""))
413
540
  if source.strip().startswith("# BEGIN QUESTION"):
414
- question_name = re.search(r"name:\s*(.*)", source)
415
- question_name = (
416
- question_name.group(1).strip() if question_name else None
417
- )
541
+ name_match = re.search(r"name:\s*(.*)", source)
542
+ question_name = name_match.group(1).strip() if name_match else None
418
543
 
419
544
  elif cell.get("cell_type") == "code":
420
545
  source = "".join(cell.get("source", ""))
@@ -77,15 +77,21 @@ class NotebookProcessor:
77
77
  data = yaml.safe_load(file)
78
78
  # Extract assignment details
79
79
  assignment = data.get("assignment", {})
80
- week_num = assignment.get("week")
80
+ self.week_num = assignment.get("week")
81
81
  self.assignment_type = assignment.get("assignment_type")
82
82
  self.bonus_points = assignment.get("bonus_points", 0)
83
+ self.require_key = assignment.get("require_key", False)
84
+ self.assignment_tag = assignment.get(
85
+ "assignment_tag",
86
+ f"week{assignment.get("week")}-{self.assignment_type}",
87
+ )
83
88
  else:
84
89
  self.assignment_type = self.assignment_tag.split("-")[0].lower()
85
- week_num = self.assignment_tag.split("-")[-1]
90
+ self.week_num = self.assignment_tag.split("-")[-1]
91
+ self.assignment_tag = f"week{self.week_num}-{self.assignment_type}"
86
92
 
87
- self.week_num = week_num
88
- self.week = f"week_{week_num}"
93
+ # self.week_num = week_num
94
+ self.week = f"week_{self.week_num}"
89
95
 
90
96
  # Define the folder to store solutions and ensure it exists
91
97
  self.solutions_folder = os.path.join(self.root_folder, "_solutions")
@@ -179,12 +185,12 @@ class NotebookProcessor:
179
185
 
180
186
  def update_initialize_function(self):
181
187
  for key, value in self.total_point_log.items():
182
- assignment_tag = f"week{self.week_num}-{self.assignment_type}"
188
+ # assignment_tag = f"week{self.week_num}-{self.assignment_type}"
183
189
 
184
190
  update_initialize_assignment(
185
191
  notebook_path=os.path.join(self.root_folder, key + ".ipynb"),
186
192
  assignment_points=value,
187
- assignment_tag=assignment_tag,
193
+ assignment_tag=self.assignment_tag,
188
194
  )
189
195
 
190
196
  def build_payload(self, yaml_content):
@@ -578,17 +584,17 @@ class NotebookProcessor:
578
584
  code_cell = nbformat.v4.new_code_cell(
579
585
  f"{validate_token_line}\n\n" # Add the validate_token() line
580
586
  "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
581
- f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
587
+ f'submit_assignment("{self.assignment_tag}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
582
588
  )
583
589
  else:
584
590
  # Define the Code cell without validate_token()
585
591
  code_cell = nbformat.v4.new_code_cell(
586
592
  "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
587
- f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
593
+ f'submit_assignment("{self.assignment_tag}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
588
594
  )
589
595
 
590
596
  # Make the code cell non-editable and non-deletable
591
- code_cell.metadata = {"editable": False, "deletable": False}
597
+ code_cell.metadata = {"editable": True, "deletable": False}
592
598
  code_cell.metadata["tags"] = ["skip-execution"]
593
599
 
594
600
  # Add the cells to the notebook
@@ -751,6 +757,8 @@ class NotebookProcessor:
751
757
  print("require_key is False. No changes made to the notebook.")
752
758
  return
753
759
 
760
+ NotebookProcessor.add_validate_block(notebook_path, require_key)
761
+
754
762
  # Load the notebook
755
763
  with open(notebook_path, "r", encoding="utf-8") as f:
756
764
  notebook = nbformat.read(f, as_version=4)
@@ -768,6 +776,39 @@ class NotebookProcessor:
768
776
  with open(notebook_path, "w", encoding="utf-8") as f:
769
777
  nbformat.write(notebook, f)
770
778
 
779
+ @staticmethod
780
+ def add_validate_block(notebook_path: str, require_key: bool) -> None:
781
+ """
782
+ Modifies the first code cell of a Jupyter notebook to add the validate_token call if require_key is True.
783
+
784
+ Args:
785
+ notebook_path (str): The path to the notebook file to modify.
786
+ require_key (bool): Whether to add the validate_token cell.
787
+
788
+ Returns:
789
+ None
790
+ """
791
+ if not require_key:
792
+ return
793
+
794
+ # Load the notebook
795
+ with open(notebook_path, "r", encoding="utf-8") as f:
796
+ notebook = nbformat.read(f, as_version=4)
797
+
798
+ # Prepare the validation code
799
+ validation_code = "validate_token()\n"
800
+
801
+ # Modify the first cell if it's a code cell, otherwise insert a new one
802
+ if notebook.cells and notebook.cells[0].cell_type == "code":
803
+ notebook.cells[0].source = validation_code + "\n" + notebook.cells[0].source
804
+ else:
805
+ new_cell = nbformat.v4.new_code_cell(validation_code)
806
+ notebook.cells.insert(0, new_cell)
807
+
808
+ # Save the modified notebook
809
+ with open(notebook_path, "w", encoding="utf-8") as f:
810
+ nbformat.write(notebook, f)
811
+
771
812
  @staticmethod
772
813
  def add_initialization_code(
773
814
  notebook_path, week, assignment_type, require_key=False
pykubegrader/telemetry.py CHANGED
@@ -303,7 +303,7 @@ def get_assignments_submissions():
303
303
  from_env = os.getenv("JUPYTERHUB_USER")
304
304
  if from_hostname != from_env:
305
305
  raise ValueError("Problem with JupyterHub username")
306
-
306
+ print(from_env)
307
307
  params = {"username": from_env}
308
308
  # get submission information
309
309
  res = requests.get(
@@ -319,21 +319,33 @@ def setup_grades_df(assignments):
319
319
 
320
320
  inds = [f"week{i + 1}" for i in range(11)] + ["Running Avg"]
321
321
  restruct_grades = {k: [0 for i in range(len(inds))] for k in assignment_types}
322
- restruct_grades["inds"] = inds
323
- new_weekly_grades = pd.DataFrame(restruct_grades)
322
+ new_weekly_grades = pd.DataFrame(restruct_grades,dtype=float)
323
+ new_weekly_grades["inds"] = inds
324
324
  new_weekly_grades.set_index("inds", inplace=True)
325
325
  return new_weekly_grades
326
326
 
327
327
 
328
+ def skipped_assignment_mask(assignments):
329
+ existing_assignment_mask = setup_grades_df(assignments).astype(bool)
330
+ for assignment in assignments:
331
+ # existing_assignment_mask[assignment["assignment_type"]].iloc[assignment["week_number"]-1] = True
332
+ existing_assignment_mask.loc[f'week{assignment["week_number"]}', assignment["assignment_type"]] = True
333
+ return existing_assignment_mask.astype(bool)
334
+
328
335
  def fill_grades_df(new_weekly_grades, assignments, student_subs):
329
336
  for assignment in assignments:
330
337
  # get the assignment from all submissions
331
- subs = [
332
- sub
333
- for sub in student_subs
334
- if sub["assignment_type"] == assignment["assignment_type"]
335
- and sub["week_number"] == assignment["week_number"]
336
- ]
338
+ subs = [ sub for sub in student_subs if (sub['assignment_type']==assignment['assignment_type']) and (sub['week_number']==assignment['week_number']) ]
339
+ # print(assignment, subs)
340
+ # print(assignment)
341
+ # print(student_subs[:5])
342
+ if assignment["assignment_type"] == "lecture":
343
+ if sum([sub["raw_score"] for sub in subs]) > 0: # TODO: good way to check for completion?
344
+ new_weekly_grades.loc[f"week{assignment['week_number']}", "lecture"] = 1.0
345
+ if assignment["assignment_type"] == "final":
346
+ continue
347
+ if assignment["assignment_type"] == "midterm":
348
+ continue
337
349
  if len(subs) == 0:
338
350
  # print(assignment['title'], 0, assignment['max_score'])
339
351
  continue
@@ -363,22 +375,18 @@ def fill_grades_df(new_weekly_grades, assignments, student_subs):
363
375
  f"week{assignment['week_number']}", assignment["assignment_type"]
364
376
  ] = grade
365
377
 
366
- # Merge different names
367
- new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(
368
- axis=1
369
- )
370
- new_weekly_grades["practicequiz"] = new_weekly_grades[
371
- ["practicequiz", "practice-quiz"]
372
- ].max(axis=1)
373
- new_weekly_grades.drop(
374
- ["attendance", "practice-quiz", "test"],
375
- axis=1,
376
- inplace=True,
377
- errors="ignore",
378
- )
379
-
380
- return new_weekly_grades
378
+ # Merge different names
379
+ new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(axis=1)
380
+ new_weekly_grades["practicequiz"] = new_weekly_grades[["practicequiz", "practice-quiz"]].max(axis=1)
381
+ new_weekly_grades["practicemidterm"] = new_weekly_grades[["practicemidterm", "PracticeMidterm"]].max(axis=1)
382
+ new_weekly_grades.drop(
383
+ ["attendance", "practice-quiz", "test", "PracticeMidterm"],
384
+ axis=1,
385
+ inplace=True,
386
+ errors="ignore",
387
+ )
381
388
 
389
+ return new_weekly_grades
382
390
 
383
391
  def get_current_week(start_date):
384
392
  # Calculate the current week (1-based indexing)
@@ -388,13 +396,36 @@ def get_current_week(start_date):
388
396
  return days_since_start // 7 + 1
389
397
 
390
398
 
399
+ def get_average_weighted_grade(assignments, current_week, new_weekly_grades, weights):
400
+ # Get average until current week
401
+ skip_weeks = skipped_assignment_mask(assignments)
402
+ for col in new_weekly_grades.columns:
403
+ new_weekly_grades.loc["Running Avg", col] = new_weekly_grades.loc[skip_weeks[col]==True, col].mean()
404
+ # for col in new_weekly_grades.columns:
405
+ # skip_weeks = skipped_assignment_mask(assignments)
406
+ # skip_weeks_series = pd.Series(skip_weeks)
407
+ # # new_weekly_grades.iloc[-1,col] = new_weekly_grades.iloc[skip_weeks_series[col],-1].mean()
408
+ # new_weekly_grades
409
+
410
+ # make new dataframe with the midterm, final, and running average
411
+ total = 0
412
+ avg_grades_dict = {}
413
+ for k, v in weights.items():
414
+ grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
415
+ total += grade * v
416
+ avg_grades_dict[k] = grade
417
+ avg_grades_dict['Total'] = total # excluded midterm and final
418
+
419
+ return avg_grades_dict
420
+
421
+
391
422
  # This function currently has many undefined variables and other problems!
392
- def get_my_grades_testing(start_date="2025-01-06"):
423
+ def get_my_grades_testing(start_date="2025-01-06", verbose=True):
393
424
  """takes in json.
394
425
  reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
395
426
  fills in 0 for missing assignments
396
427
  calculate running average of each category"""
397
-
428
+
398
429
  # set up new df format
399
430
  weights = {
400
431
  "homework": 0.15,
@@ -408,23 +439,71 @@ def get_my_grades_testing(start_date="2025-01-06"):
408
439
  }
409
440
 
410
441
  assignments, student_subs = get_assignments_submissions()
411
-
442
+
412
443
  new_grades_df = setup_grades_df(assignments)
413
444
 
414
445
  new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
415
446
 
416
447
  current_week = get_current_week(start_date)
448
+
449
+ avg_grades_dict = get_average_weighted_grade(assignments, current_week, new_weekly_grades, weights)
450
+
451
+ if verbose:
452
+ max_key_length = max(len(k) for k in weights.keys())
453
+ for k, v in avg_grades_dict.items():
454
+ print(f'{k:<{max_key_length}}:\t {v:.2f}')
417
455
 
418
- # Get average until current week
419
- new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
456
+ return new_weekly_grades # get rid of test and running avg columns
457
+ def get_all_students(admin_user, admin_pw):
458
+ res = requests.get(
459
+ url=api_base_url.rstrip("/") + "/students",
460
+ auth=HTTPBasicAuth(admin_user, admin_pw),
461
+ )
462
+ res.raise_for_status()
420
463
 
421
- # make new dataframe with the midterm, final, and running average
422
- max_key_length = max(len(k) for k in weights.keys())
423
- total = 0
424
- for k, v in weights.items():
425
- grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
426
- total += grade * v
427
- print(f"{k:<{max_key_length}}:\t {grade:.2f}")
428
- print(f"\nTotal: {total}") # exclude midterm and final
464
+ # Input: List of players
465
+ return [student['email'].split('@')[0] for student in res.json()]
429
466
 
430
- return new_weekly_grades # get rid of test and running avg columns
467
+
468
+ # def all_student_grades_testing(admin_user, admin_pw, start_date="2025-01-06"):
469
+ # """takes in json.
470
+ # reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
471
+ # fills in 0 for missing assignments
472
+ # calculate running average of each category"""
473
+
474
+ # # set up new df format
475
+ # weights = {
476
+ # "homework": 0.15,
477
+ # "lab": 0.15,
478
+ # "lecture": 0.15,
479
+ # "quiz": 0.15,
480
+ # "readings": 0.15,
481
+ # # 'midterm':0.15, 'final':0.2
482
+ # "labattendance": 0.05,
483
+ # "practicequiz": 0.05,
484
+ # }
485
+
486
+ # student_usernames = get_student_usernames(admin_user, admin_pw)
487
+
488
+ # assignments, student_subs = get_assignments_submissions(admin_user, admin_pw)
489
+
490
+ # new_grades_df = setup_grades_df(assignments)
491
+
492
+ # new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
493
+
494
+ # current_week = get_current_week(start_date)
495
+
496
+ # # Get average until current week
497
+ # new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
498
+
499
+ # # make new dataframe with the midterm, final, and running average
500
+ # max_key_length = max(len(k) for k in weights.keys())
501
+ # total = 0
502
+ # for k, v in weights.items():
503
+ # grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
504
+ # total += grade * v
505
+ # print(f"{k:<{max_key_length}}:\t {grade:.2f}")
506
+ # print(f"\nTotal: {total}") # exclude midterm and final
507
+
508
+ # return new_weekly_grades # get rid of test and running avg columns
509
+
@@ -0,0 +1,53 @@
1
+ import panel as pn
2
+ import requests
3
+ from requests.auth import HTTPBasicAuth
4
+ import os
5
+
6
+ from ..utils import api_base_url
7
+
8
+ # Dummy credentials for HTTP Basic Auth
9
+ AUTH = HTTPBasicAuth("user", "password")
10
+
11
+ # Panel configuration
12
+ pn.extension()
13
+
14
+
15
+ def get_jhub_user():
16
+ """
17
+ Fetches the JupyterHub user from the environment.
18
+ """
19
+ jhub_user = os.getenv("JUPYTERHUB_USER")
20
+ if jhub_user is None:
21
+ raise ValueError("JupyterHub user not found")
22
+ return jhub_user
23
+
24
+
25
+ def get_students():
26
+
27
+ # Make the request
28
+ response = requests.get(
29
+ f"{api_base_url}students",
30
+ auth=HTTPBasicAuth("user", "pass"), # Basic Auth
31
+ params={"requester": get_jhub_user()}, # Query parameter
32
+ )
33
+
34
+ # Print response
35
+ if response.status_code == 200:
36
+ return [student["email"].split("@")[0] for student in response.json()]
37
+ else:
38
+ print(f"Error {response.status_code}: {response.text}")
39
+
40
+
41
+ def get_assignments():
42
+ # Make the request
43
+ response = requests.get(
44
+ f"{api_base_url}assignments",
45
+ auth=HTTPBasicAuth("user", "pass"), # Basic Auth
46
+ params={"requester": get_jhub_user()}, # Query parameter
47
+ )
48
+
49
+ # Print response
50
+ if response.status_code == 200:
51
+ return [assignment["title"] for assignment in response.json()]
52
+ else:
53
+ print(f"Error {response.status_code}: {response.text}")
@@ -6,19 +6,30 @@ from requests.auth import HTTPBasicAuth
6
6
  from ..utils import api_base_url
7
7
 
8
8
 
9
- def build_token_payload(token: str, duration: int) -> dict:
9
+ def build_token_payload(token: str, duration: int, **kwargs) -> dict:
10
+
11
+ student_id = kwargs.get("student_id", None)
12
+ assignment = kwargs.get("assignment", None)
13
+
10
14
  jhub_user = os.getenv("JUPYTERHUB_USER")
11
15
  if jhub_user is None:
12
16
  raise ValueError("JupyterHub user not found")
13
17
 
14
- return {
18
+ payload = {
15
19
  "value": token,
16
20
  "duration": duration,
17
21
  "requester": jhub_user,
18
22
  }
19
23
 
24
+ if student_id:
25
+ payload["student_id"] = student_id
26
+ if assignment:
27
+ payload["assignment"] = assignment
28
+
29
+ return payload
30
+
20
31
 
21
- def add_token(token: str, duration: int = 20) -> None:
32
+ def add_token(token: str, duration: int = 20, **kwargs) -> None:
22
33
  """
23
34
  Sends a POST request to mint a token
24
35
  """
@@ -27,7 +38,7 @@ def add_token(token: str, duration: int = 20) -> None:
27
38
  raise ValueError("Environment variable for API URL not set")
28
39
  url = api_base_url.rstrip("/") + "/tokens"
29
40
 
30
- payload = build_token_payload(token=token, duration=duration)
41
+ payload = build_token_payload(token=token, duration=duration, **kwargs)
31
42
 
32
43
  # Dummy credentials for HTTP Basic Auth
33
44
  auth = HTTPBasicAuth("user", "password")
@@ -6,6 +6,16 @@ import requests
6
6
  from requests.auth import HTTPBasicAuth
7
7
 
8
8
 
9
+ def get_jhub_user():
10
+ """
11
+ Fetches the JupyterHub user from the environment.
12
+ """
13
+ jhub_user = os.getenv("JUPYTERHUB_USER")
14
+ if jhub_user is None:
15
+ raise ValueError("JupyterHub user not found")
16
+ return jhub_user
17
+
18
+
9
19
  class TokenValidationError(Exception):
10
20
  """
11
21
  Custom exception raised when the token validation fails.
@@ -41,7 +51,10 @@ def get_credentials() -> dict[str, str]:
41
51
  return {"username": username, "password": password}
42
52
 
43
53
 
44
- def validate_token(token: Optional[str] = None) -> None:
54
+ def validate_token(
55
+ token: Optional[str] = None,
56
+ assignment: Optional[str] = None,
57
+ ) -> None:
45
58
  if token:
46
59
  os.environ["TOKEN"] = token # If token passed, set env var
47
60
  else:
@@ -55,8 +68,17 @@ def validate_token(token: Optional[str] = None) -> None:
55
68
  if not base_url:
56
69
  print("Error: Environment variable 'DB_URL' not set", file=sys.stderr)
57
70
  sys.exit(1)
71
+
72
+ # Construct endpoint with optional parameters
58
73
  endpoint = f"{base_url.rstrip('/')}/validate-token/{token}"
59
74
 
75
+ # Build query parameters
76
+ params = {}
77
+ if assignment:
78
+ params["assignment"] = assignment
79
+
80
+ params["student_id"] = get_jhub_user()
81
+
60
82
  # Get credentials
61
83
  try:
62
84
  credentials = get_credentials()
@@ -69,7 +91,10 @@ def validate_token(token: Optional[str] = None) -> None:
69
91
  basic_auth = HTTPBasicAuth(username, password)
70
92
 
71
93
  try:
72
- response = requests.get(url=endpoint, auth=basic_auth, timeout=10)
94
+ # Send request with optional query parameters
95
+ response = requests.get(
96
+ url=endpoint, auth=basic_auth, timeout=10, params=params
97
+ )
73
98
  response.raise_for_status()
74
99
 
75
100
  detail = response.json().get("detail", response.text)
@@ -2,6 +2,7 @@ import time
2
2
  from typing import Callable, Optional, Tuple
3
3
 
4
4
  import panel as pn
5
+ import numpy as np
5
6
 
6
7
  from ..telemetry import ensure_responses, score_question, update_responses
7
8
  from ..utils import shuffle_options, shuffle_questions
@@ -70,7 +71,7 @@ class MultiSelectQuestion:
70
71
 
71
72
  # Panel layout
72
73
  question_header = pn.pane.HTML(
73
- f"<h2>Question {self.question_number}: {title}</h2>"
74
+ f"<h2>Question {self.question_number} (Points {np.sum(points)}): {title}</h2>"
74
75
  )
75
76
 
76
77
  question_body = pn.Column(
@@ -2,6 +2,7 @@ import time
2
2
  from typing import Callable, Optional, Tuple
3
3
 
4
4
  import panel as pn
5
+ import numpy as np
5
6
 
6
7
  from ..telemetry import ensure_responses, score_question, update_responses
7
8
  from ..utils import shuffle_options, shuffle_questions
@@ -57,7 +58,7 @@ class SelectQuestion:
57
58
  widget_pairs = shuffle_questions(desc_widgets, self.widgets, seed)
58
59
 
59
60
  self.layout = pn.Column(
60
- f"# Question {self.question_number}: {title}",
61
+ f"# Question {self.question_number} (Points {np.sum(points)}): {title}",
61
62
  *(
62
63
  pn.Column(desc_widget, pn.Row(dropdown))
63
64
  for desc_widget, dropdown in widget_pairs