PyKubeGrader 0.3.5__py3-none-any.whl → 0.3.7__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.5.dist-info → PyKubeGrader-0.3.7.dist-info}/METADATA +1 -1
- {PyKubeGrader-0.3.5.dist-info → PyKubeGrader-0.3.7.dist-info}/RECORD +13 -12
- pykubegrader/build/api_notebook_builder.py +78 -15
- pykubegrader/build/build_folder.py +64 -15
- pykubegrader/submit/submit_assignment.py +3 -0
- 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-0.3.5.dist-info → PyKubeGrader-0.3.7.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.3.5.dist-info → PyKubeGrader-0.3.7.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.3.5.dist-info → PyKubeGrader-0.3.7.dist-info}/entry_points.txt +0 -0
- {PyKubeGrader-0.3.5.dist-info → PyKubeGrader-0.3.7.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=HDccW597shBhAd-jxvMUPpsiNj2S4DlKRGMDH_6U9nQ,87590
|
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
|
@@ -16,9 +16,10 @@ pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl
|
|
16
16
|
pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
17
17
|
pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
|
18
18
|
pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
|
19
|
-
pykubegrader/submit/submit_assignment.py,sha256=
|
20
|
-
pykubegrader/tokens/
|
21
|
-
pykubegrader/tokens/
|
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
|
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
|
@@ -32,9 +33,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
|
|
32
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
35
|
pykubegrader/widgets_base/select.py,sha256=uMncmVIqjvJkffMQY1L_PokrFCidK1PeVITX0i70fho,2750
|
35
|
-
PyKubeGrader-0.3.
|
36
|
-
PyKubeGrader-0.3.
|
37
|
-
PyKubeGrader-0.3.
|
38
|
-
PyKubeGrader-0.3.
|
39
|
-
PyKubeGrader-0.3.
|
40
|
-
PyKubeGrader-0.3.
|
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,,
|
@@ -17,12 +17,15 @@ class FastAPINotebookBuilder:
|
|
17
17
|
temp_notebook: Optional[str] = None
|
18
18
|
assignment_tag: Optional[str] = ""
|
19
19
|
require_key: Optional[bool] = False
|
20
|
+
verbose: Optional[bool] = False
|
20
21
|
|
21
22
|
def __post_init__(self) -> None:
|
22
23
|
self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
|
23
24
|
self.notebook_path
|
24
25
|
)
|
25
26
|
self.total_points = 0
|
27
|
+
|
28
|
+
self.max_question_points = {}
|
26
29
|
self.run()
|
27
30
|
|
28
31
|
def run(self) -> None:
|
@@ -38,6 +41,9 @@ class FastAPINotebookBuilder:
|
|
38
41
|
self.assertion_tests_dict = self.question_dict()
|
39
42
|
self.add_api_code()
|
40
43
|
|
44
|
+
# add the point total to the end of the notebook
|
45
|
+
self.add_total_points_to_notebook()
|
46
|
+
|
41
47
|
@staticmethod
|
42
48
|
def conceal_tests(cell_source):
|
43
49
|
"""
|
@@ -76,28 +82,33 @@ class FastAPINotebookBuilder:
|
|
76
82
|
|
77
83
|
return concealed_lines
|
78
84
|
|
79
|
-
def add_api_code(self):
|
80
85
|
def add_api_code(self) -> None:
|
81
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
|
82
94
|
|
83
95
|
for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
|
84
|
-
|
85
|
-
|
86
|
-
|
96
|
+
if self.verbose:
|
97
|
+
print(
|
98
|
+
f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
|
99
|
+
)
|
87
100
|
|
88
101
|
cell = self.get_cell(cell_index)
|
89
102
|
cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
|
90
|
-
cell["source"]
|
103
|
+
cell["source"], require_key=self.require_key,
|
91
104
|
)
|
92
|
-
|
105
|
+
|
93
106
|
cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
|
94
107
|
|
95
108
|
last_import_line_ind = FastAPINotebookBuilder.find_last_import_line(
|
96
109
|
cell_source
|
97
110
|
)
|
98
111
|
|
99
|
-
# header, body = FastAPINotebookBuilder.split_list_at_marker(cell_source)
|
100
|
-
|
101
112
|
updated_cell_source = []
|
102
113
|
updated_cell_source.extend(cell_source[: last_import_line_ind + 1])
|
103
114
|
if cell_dict["is_first"]:
|
@@ -135,18 +146,65 @@ class FastAPINotebookBuilder:
|
|
135
146
|
FastAPINotebookBuilder.construct_update_responses(cell_dict)
|
136
147
|
)
|
137
148
|
|
138
|
-
self.replace_cell_source(cell_index, updated_cell_source)
|
149
|
+
self.replace_cell_source(cell_index, updated_cell_source)
|
150
|
+
|
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(
|
170
|
+
cell["points"]
|
171
|
+
for cell in self.assertion_tests_dict.values()
|
172
|
+
if cell["question"] == cell_dict["question"]
|
173
|
+
)
|
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
|
139
198
|
|
140
199
|
def compute_max_points_free_response(self) -> None:
|
141
200
|
for cell_dict in self.assertion_tests_dict.values():
|
142
201
|
# gets the question name from the first cell to not double count
|
143
202
|
if cell_dict["is_first"]:
|
144
203
|
# get the max points for the question
|
145
|
-
max_question_points =
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
)
|
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
|
150
208
|
|
151
209
|
self.total_points += max_question_points
|
152
210
|
|
@@ -272,7 +330,7 @@ class FastAPINotebookBuilder:
|
|
272
330
|
return original_list[:index] + insert_list + original_list[index:]
|
273
331
|
|
274
332
|
@staticmethod
|
275
|
-
def add_import_statements_to_tests(cell_source: list[str]) -> list[str]:
|
333
|
+
def add_import_statements_to_tests(cell_source: list[str], require_key:bool = False) -> list[str]:
|
276
334
|
"""
|
277
335
|
Adds the necessary import statements to the first cell of the notebook.
|
278
336
|
"""
|
@@ -293,6 +351,11 @@ class FastAPINotebookBuilder:
|
|
293
351
|
"import base64\n",
|
294
352
|
]
|
295
353
|
|
354
|
+
if require_key:
|
355
|
+
imports.append(
|
356
|
+
"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
|
357
|
+
)
|
358
|
+
|
296
359
|
for i, line in enumerate(cell_source):
|
297
360
|
if end_test_config_line in line:
|
298
361
|
# Insert the imports immediately after the current line
|
@@ -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):
|
@@ -572,23 +578,23 @@ class NotebookProcessor:
|
|
572
578
|
|
573
579
|
if self.require_key:
|
574
580
|
# Add an additional line for validate_token()
|
575
|
-
validate_token_line =
|
581
|
+
validate_token_line =f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
|
576
582
|
|
577
583
|
# Define the Code cell
|
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
|
@@ -675,6 +681,7 @@ class NotebookProcessor:
|
|
675
681
|
self.week,
|
676
682
|
self.assignment_type,
|
677
683
|
require_key=self.require_key,
|
684
|
+
assignment_tag = self.assignment_tag,
|
678
685
|
)
|
679
686
|
|
680
687
|
NotebookProcessor.replace_temp_in_notebook(
|
@@ -705,6 +712,7 @@ class NotebookProcessor:
|
|
705
712
|
self.week,
|
706
713
|
self.assignment_type,
|
707
714
|
require_key=self.require_key,
|
715
|
+
assignment_tag = self.assignment_tag
|
708
716
|
)
|
709
717
|
NotebookProcessor.replace_temp_no_otter(
|
710
718
|
temp_notebook_path, temp_notebook_path
|
@@ -736,7 +744,7 @@ class NotebookProcessor:
|
|
736
744
|
nbformat.write(notebook, f)
|
737
745
|
|
738
746
|
@staticmethod
|
739
|
-
def add_validate_token_cell(notebook_path: str, require_key: bool) -> None:
|
747
|
+
def add_validate_token_cell(notebook_path: str, require_key: bool, **kwargs) -> None:
|
740
748
|
"""
|
741
749
|
Adds a new code cell at the top of a Jupyter notebook if require_key is True.
|
742
750
|
|
@@ -750,16 +758,24 @@ class NotebookProcessor:
|
|
750
758
|
if not require_key:
|
751
759
|
print("require_key is False. No changes made to the notebook.")
|
752
760
|
return
|
761
|
+
|
762
|
+
NotebookProcessor.add_validate_block(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
|
753
763
|
|
754
764
|
# Load the notebook
|
755
765
|
with open(notebook_path, "r", encoding="utf-8") as f:
|
756
766
|
notebook = nbformat.read(f, as_version=4)
|
757
767
|
|
758
768
|
# Create the new code cell
|
759
|
-
|
769
|
+
if kwargs.get("assignment_tag", None):
|
770
|
+
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"
|
773
|
+
)
|
774
|
+
else:
|
775
|
+
new_cell = nbformat.v4.new_code_cell(
|
760
776
|
"from pykubegrader.tokens.validate_token import validate_token\n"
|
761
777
|
"validate_token('type the key provided by your instructor here')\n"
|
762
|
-
|
778
|
+
)
|
763
779
|
|
764
780
|
# Add the new cell to the top of the notebook
|
765
781
|
notebook.cells.insert(0, new_cell)
|
@@ -768,9 +784,42 @@ class NotebookProcessor:
|
|
768
784
|
with open(notebook_path, "w", encoding="utf-8") as f:
|
769
785
|
nbformat.write(notebook, f)
|
770
786
|
|
787
|
+
@staticmethod
|
788
|
+
def add_validate_block(notebook_path: str, require_key: bool, **kwargs) -> None:
|
789
|
+
"""
|
790
|
+
Modifies the first code cell of a Jupyter notebook to add the validate_token call if require_key is True.
|
791
|
+
|
792
|
+
Args:
|
793
|
+
notebook_path (str): The path to the notebook file to modify.
|
794
|
+
require_key (bool): Whether to add the validate_token cell.
|
795
|
+
|
796
|
+
Returns:
|
797
|
+
None
|
798
|
+
"""
|
799
|
+
if not require_key:
|
800
|
+
return
|
801
|
+
|
802
|
+
# Load the notebook
|
803
|
+
with open(notebook_path, "r", encoding="utf-8") as f:
|
804
|
+
notebook = nbformat.read(f, as_version=4)
|
805
|
+
|
806
|
+
# Prepare the validation code
|
807
|
+
validation_code = "validate_token()\n"
|
808
|
+
|
809
|
+
# Modify the first cell if it's a code cell, otherwise insert a new one
|
810
|
+
if notebook.cells and notebook.cells[0].cell_type == "code":
|
811
|
+
notebook.cells[0].source = validation_code + "\n" + notebook.cells[0].source
|
812
|
+
else:
|
813
|
+
new_cell = nbformat.v4.new_code_cell(validation_code)
|
814
|
+
notebook.cells.insert(0, new_cell)
|
815
|
+
|
816
|
+
# Save the modified notebook
|
817
|
+
with open(notebook_path, "w", encoding="utf-8") as f:
|
818
|
+
nbformat.write(notebook, f)
|
819
|
+
|
771
820
|
@staticmethod
|
772
821
|
def add_initialization_code(
|
773
|
-
notebook_path, week, assignment_type, require_key=False
|
822
|
+
notebook_path, week, assignment_type, require_key=False, **kwargs,
|
774
823
|
):
|
775
824
|
# finds the first code cell
|
776
825
|
index, cell = find_first_code_cell(notebook_path)
|
@@ -782,7 +831,7 @@ class NotebookProcessor:
|
|
782
831
|
replace_cell_source(notebook_path, index, cell)
|
783
832
|
|
784
833
|
if require_key:
|
785
|
-
NotebookProcessor.add_validate_token_cell(notebook_path, require_key)
|
834
|
+
NotebookProcessor.add_validate_token_cell(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
|
786
835
|
|
787
836
|
def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
|
788
837
|
### Parse the notebook for multiple choice questions
|
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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|