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.
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/METADATA +1 -1
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/RECORD +14 -13
- pykubegrader/build/api_notebook_builder.py +162 -37
- pykubegrader/build/build_folder.py +50 -9
- pykubegrader/telemetry.py +117 -38
- pykubegrader/tokens/token_panel.py +53 -0
- pykubegrader/tokens/tokens.py +15 -4
- pykubegrader/tokens/validate_token.py +27 -2
- pykubegrader/widgets_base/multi_select.py +2 -1
- pykubegrader/widgets_base/select.py +2 -1
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/entry_points.txt +0 -0
- {PyKubeGrader-0.3.4.dist-info → PyKubeGrader-0.3.6.dist-info}/top_level.txt +0 -0
@@ -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=
|
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=
|
9
|
-
pykubegrader/build/build_folder.py,sha256=
|
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/
|
21
|
-
pykubegrader/tokens/
|
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=
|
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=
|
35
|
-
PyKubeGrader-0.3.
|
36
|
-
PyKubeGrader-0.3.
|
37
|
-
PyKubeGrader-0.3.
|
38
|
-
PyKubeGrader-0.3.
|
39
|
-
PyKubeGrader-0.3.
|
40
|
-
PyKubeGrader-0.3.
|
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
|
-
|
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
|
-
|
44
|
-
|
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
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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(
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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("
|
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("
|
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":
|
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
|
-
|
323
|
-
new_weekly_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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
#
|
419
|
-
|
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
|
-
#
|
422
|
-
|
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
|
-
|
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}")
|
pykubegrader/tokens/tokens.py
CHANGED
@@ -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
|
-
|
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(
|
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
|
-
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|