PyKubeGrader 0.3.3__py3-none-any.whl → 0.3.5__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.3.dist-info → PyKubeGrader-0.3.5.dist-info}/METADATA +1 -4
- {PyKubeGrader-0.3.3.dist-info → PyKubeGrader-0.3.5.dist-info}/RECORD +14 -13
- {PyKubeGrader-0.3.3.dist-info → PyKubeGrader-0.3.5.dist-info}/entry_points.txt +1 -0
- pykubegrader/build/api_notebook_builder.py +86 -24
- pykubegrader/build/collate.py +190 -0
- pykubegrader/grade_reports/grade_reports.py +12 -107
- pykubegrader/grading_tester.ipynb +47 -36
- pykubegrader/submit/submit_assignment.py +42 -58
- pykubegrader/telemetry.py +146 -1
- pykubegrader/widgets_base/multi_select.py +2 -1
- pykubegrader/widgets_base/select.py +2 -1
- {PyKubeGrader-0.3.3.dist-info → PyKubeGrader-0.3.5.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.3.3.dist-info → PyKubeGrader-0.3.5.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.3.3.dist-info → PyKubeGrader-0.3.5.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.5
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -12,12 +12,10 @@ Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python
|
13
13
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
14
14
|
License-File: LICENSE.txt
|
15
|
-
Requires-Dist: httpx
|
16
15
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
17
16
|
Requires-Dist: ipython
|
18
17
|
Requires-Dist: mypy
|
19
18
|
Requires-Dist: nbformat
|
20
|
-
Requires-Dist: nest_asyncio
|
21
19
|
Requires-Dist: numpy
|
22
20
|
Requires-Dist: pandas-stubs
|
23
21
|
Requires-Dist: panel
|
@@ -28,7 +26,6 @@ Requires-Dist: ruff
|
|
28
26
|
Requires-Dist: setuptools
|
29
27
|
Requires-Dist: sphinx
|
30
28
|
Requires-Dist: types-python-dateutil
|
31
|
-
Requires-Dist: types-pyyaml
|
32
29
|
Requires-Dist: types-requests
|
33
30
|
Requires-Dist: types-setuptools
|
34
31
|
Provides-Extra: testing
|
@@ -1,21 +1,22 @@
|
|
1
1
|
pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
|
2
|
-
pykubegrader/grading_tester.ipynb,sha256=
|
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=ooLK-dY_hJQ7t4r83hWyO8wx6F_7TfWJS7tCp_nH7r8,13049
|
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=
|
8
|
+
pykubegrader/build/api_notebook_builder.py,sha256=320aFB1-6KLQt0OMmKD0FJtE17JKZaK5neA0mTD-Y8M,22960
|
9
9
|
pykubegrader/build/build_folder.py,sha256=Asc-VdhXgxQfOfFIWJShhXrF2EITJOIZQ5Dz_2y-P2I,85358
|
10
10
|
pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
|
11
|
+
pykubegrader/build/collate.py,sha256=cVvF7tf2U3iiH4R_dbghTcieedIx5w3Fyw9L_llInM8,6754
|
11
12
|
pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
|
12
|
-
pykubegrader/grade_reports/grade_reports.py,sha256=
|
13
|
+
pykubegrader/grade_reports/grade_reports.py,sha256=n8H_n9jdZRSPn2zlIf-GQt_Y8w91p6M8ZbdVH76Sg5k,2303
|
13
14
|
pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
14
15
|
pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
|
15
16
|
pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
16
17
|
pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
|
17
18
|
pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
|
18
|
-
pykubegrader/submit/submit_assignment.py,sha256=
|
19
|
+
pykubegrader/submit/submit_assignment.py,sha256=cqVu7US8GVaCdJdaU2yjawlVBtAKP5XJc4oAvX5FeRU,2575
|
19
20
|
pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
|
20
21
|
pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
|
21
22
|
pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
@@ -28,12 +29,12 @@ pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs
|
|
28
29
|
pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
|
29
30
|
pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
|
30
31
|
pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
31
|
-
pykubegrader/widgets_base/multi_select.py,sha256=
|
32
|
+
pykubegrader/widgets_base/multi_select.py,sha256=KtfAP0PyEbcjWlKNpI5_5-PLMtcUbbNX0Es_-w-H34Q,4226
|
32
33
|
pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
|
33
|
-
pykubegrader/widgets_base/select.py,sha256=
|
34
|
-
PyKubeGrader-0.3.
|
35
|
-
PyKubeGrader-0.3.
|
36
|
-
PyKubeGrader-0.3.
|
37
|
-
PyKubeGrader-0.3.
|
38
|
-
PyKubeGrader-0.3.
|
39
|
-
PyKubeGrader-0.3.
|
34
|
+
pykubegrader/widgets_base/select.py,sha256=uMncmVIqjvJkffMQY1L_PokrFCidK1PeVITX0i70fho,2750
|
35
|
+
PyKubeGrader-0.3.5.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
|
36
|
+
PyKubeGrader-0.3.5.dist-info/METADATA,sha256=a5vrrGdf4XTTSfKPAuQ-2HaHISFczwu6uSy6KkrrQv4,2729
|
37
|
+
PyKubeGrader-0.3.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
38
|
+
PyKubeGrader-0.3.5.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
|
39
|
+
PyKubeGrader-0.3.5.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
|
40
|
+
PyKubeGrader-0.3.5.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
|
|
@@ -16,14 +18,14 @@ class FastAPINotebookBuilder:
|
|
16
18
|
assignment_tag: Optional[str] = ""
|
17
19
|
require_key: Optional[bool] = False
|
18
20
|
|
19
|
-
def __post_init__(self):
|
21
|
+
def __post_init__(self) -> None:
|
20
22
|
self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
|
21
23
|
self.notebook_path
|
22
24
|
)
|
23
25
|
self.total_points = 0
|
24
26
|
self.run()
|
25
27
|
|
26
|
-
def run(self):
|
28
|
+
def run(self) -> None:
|
27
29
|
# here for easy debugging
|
28
30
|
if self.temp_notebook is not None:
|
29
31
|
shutil.copy(
|
@@ -36,7 +38,46 @@ class FastAPINotebookBuilder:
|
|
36
38
|
self.assertion_tests_dict = self.question_dict()
|
37
39
|
self.add_api_code()
|
38
40
|
|
41
|
+
@staticmethod
|
42
|
+
def conceal_tests(cell_source):
|
43
|
+
"""
|
44
|
+
Takes a list of code lines, detects blocks between `# BEGIN HIDE` and `# END HIDE`,
|
45
|
+
encodes them in Base64, and replaces them with an `exec()` statement.
|
46
|
+
|
47
|
+
Returns a new list of lines with the concealed blocks.
|
48
|
+
"""
|
49
|
+
|
50
|
+
concealed_lines = []
|
51
|
+
hide_mode = False
|
52
|
+
hidden_code = []
|
53
|
+
|
54
|
+
for line in cell_source:
|
55
|
+
if "# BEGIN HIDE" in line:
|
56
|
+
hide_mode = True
|
57
|
+
hidden_code = [] # Start a new hidden block
|
58
|
+
concealed_lines.append(line) # Keep the marker for clarity
|
59
|
+
continue
|
60
|
+
elif "# END HIDE" in line:
|
61
|
+
hide_mode = False
|
62
|
+
# Encode the entire block
|
63
|
+
encoded_block = base64.b64encode(
|
64
|
+
"\n".join(hidden_code).encode()
|
65
|
+
).decode()
|
66
|
+
concealed_lines.append(
|
67
|
+
f'exec(base64.b64decode("{encoded_block}").decode()) # Obfuscated\n'
|
68
|
+
)
|
69
|
+
concealed_lines.append(line) # Keep the marker for clarity
|
70
|
+
continue
|
71
|
+
|
72
|
+
if hide_mode:
|
73
|
+
hidden_code.append(line.strip()) # Collect hidden code
|
74
|
+
else:
|
75
|
+
concealed_lines.append(line)
|
76
|
+
|
77
|
+
return concealed_lines
|
78
|
+
|
39
79
|
def add_api_code(self):
|
80
|
+
def add_api_code(self) -> None:
|
40
81
|
self.compute_max_points_free_response()
|
41
82
|
|
42
83
|
for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
|
@@ -48,6 +89,8 @@ class FastAPINotebookBuilder:
|
|
48
89
|
cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
|
49
90
|
cell["source"]
|
50
91
|
)
|
92
|
+
|
93
|
+
cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
|
51
94
|
|
52
95
|
last_import_line_ind = FastAPINotebookBuilder.find_last_import_line(
|
53
96
|
cell_source
|
@@ -94,7 +137,7 @@ class FastAPINotebookBuilder:
|
|
94
137
|
|
95
138
|
self.replace_cell_source(cell_index, updated_cell_source)
|
96
139
|
|
97
|
-
def compute_max_points_free_response(self):
|
140
|
+
def compute_max_points_free_response(self) -> None:
|
98
141
|
for cell_dict in self.assertion_tests_dict.values():
|
99
142
|
# gets the question name from the first cell to not double count
|
100
143
|
if cell_dict["is_first"]:
|
@@ -107,7 +150,7 @@ class FastAPINotebookBuilder:
|
|
107
150
|
|
108
151
|
self.total_points += max_question_points
|
109
152
|
|
110
|
-
def construct_first_cell_question_header(self, cell_dict):
|
153
|
+
def construct_first_cell_question_header(self, cell_dict: dict) -> list[str]:
|
111
154
|
max_question_points = sum(
|
112
155
|
cell["points"]
|
113
156
|
for cell in self.assertion_tests_dict.values()
|
@@ -136,7 +179,7 @@ class FastAPINotebookBuilder:
|
|
136
179
|
return first_cell_header
|
137
180
|
|
138
181
|
@staticmethod
|
139
|
-
def construct_update_responses(cell_dict):
|
182
|
+
def construct_update_responses(cell_dict: dict) -> list[str]:
|
140
183
|
update_responses = []
|
141
184
|
|
142
185
|
logging_variables = cell_dict["logging_variables"]
|
@@ -149,7 +192,9 @@ class FastAPINotebookBuilder:
|
|
149
192
|
return update_responses
|
150
193
|
|
151
194
|
@staticmethod
|
152
|
-
def split_list_at_marker(
|
195
|
+
def split_list_at_marker(
|
196
|
+
input_list: list[str], marker: str = """# END TEST CONFIG"""
|
197
|
+
) -> tuple[list[str], list[str]]:
|
153
198
|
"""
|
154
199
|
Splits a list into two parts at the specified marker string.
|
155
200
|
|
@@ -172,7 +217,7 @@ class FastAPINotebookBuilder:
|
|
172
217
|
) # If the marker is not in the list, return the original list and an empty list
|
173
218
|
|
174
219
|
@staticmethod
|
175
|
-
def construct_graders(cell_dict):
|
220
|
+
def construct_graders(cell_dict: dict) -> list[str]:
|
176
221
|
# Generate Python code
|
177
222
|
added_code = [
|
178
223
|
"if "
|
@@ -184,7 +229,7 @@ class FastAPINotebookBuilder:
|
|
184
229
|
return added_code
|
185
230
|
|
186
231
|
@staticmethod
|
187
|
-
def construct_question_info(cell_dict):
|
232
|
+
def construct_question_info(cell_dict: dict) -> list[str]:
|
188
233
|
question_info = []
|
189
234
|
|
190
235
|
question_id = cell_dict["question"] + "-" + str(cell_dict["test_number"])
|
@@ -197,8 +242,12 @@ class FastAPINotebookBuilder:
|
|
197
242
|
|
198
243
|
@staticmethod
|
199
244
|
def insert_list_at_index(
|
200
|
-
original_list
|
201
|
-
|
245
|
+
original_list: list[str],
|
246
|
+
insert_list: list[str],
|
247
|
+
index: int,
|
248
|
+
line_break: bool = True,
|
249
|
+
inplace_line_break: bool = True,
|
250
|
+
) -> list[str]:
|
202
251
|
"""
|
203
252
|
Inserts a list into another list at a specific index.
|
204
253
|
|
@@ -223,7 +272,7 @@ class FastAPINotebookBuilder:
|
|
223
272
|
return original_list[:index] + insert_list + original_list[index:]
|
224
273
|
|
225
274
|
@staticmethod
|
226
|
-
def add_import_statements_to_tests(cell_source):
|
275
|
+
def add_import_statements_to_tests(cell_source: list[str]) -> list[str]:
|
227
276
|
"""
|
228
277
|
Adds the necessary import statements to the first cell of the notebook.
|
229
278
|
"""
|
@@ -241,6 +290,7 @@ class FastAPINotebookBuilder:
|
|
241
290
|
" update_responses,\n",
|
242
291
|
")\n",
|
243
292
|
"import os\n",
|
293
|
+
"import base64\n",
|
244
294
|
]
|
245
295
|
|
246
296
|
for i, line in enumerate(cell_source):
|
@@ -251,7 +301,12 @@ class FastAPINotebookBuilder:
|
|
251
301
|
] + imports # Add a blank line for readability
|
252
302
|
return cell_source # Exit the loop once the imports are inserted
|
253
303
|
|
254
|
-
|
304
|
+
raise ValueError("End of test configuration not found")
|
305
|
+
|
306
|
+
# TODO: `Any` return not good; would be better to specify return type(s)
|
307
|
+
def extract_first_cell(self) -> Any:
|
308
|
+
if not self.temp_notebook:
|
309
|
+
raise ValueError("No temporary notebook file path provided")
|
255
310
|
with open(self.temp_notebook, "r", encoding="utf-8") as f:
|
256
311
|
notebook = json.load(f)
|
257
312
|
if "cells" in notebook and len(notebook["cells"]) > 0:
|
@@ -260,13 +315,16 @@ class FastAPINotebookBuilder:
|
|
260
315
|
return None
|
261
316
|
|
262
317
|
@staticmethod
|
263
|
-
def get_filename_and_root(path):
|
318
|
+
def get_filename_and_root(path: str) -> tuple[Path, str]:
|
264
319
|
path_obj = Path(path).resolve() # Resolve the path to get an absolute path
|
265
320
|
root_path = path_obj.parent # Get the parent directory
|
266
321
|
filename = path_obj.name # Get the filename
|
267
322
|
return root_path, filename
|
268
323
|
|
269
|
-
|
324
|
+
# TODO: `Any` return not good; would be better to specify return type(s)
|
325
|
+
def get_cell(self, cell_index: int) -> Any:
|
326
|
+
if not self.temp_notebook:
|
327
|
+
raise ValueError("No temporary notebook file path provided")
|
270
328
|
with open(self.temp_notebook, "r", encoding="utf-8") as f:
|
271
329
|
notebook = json.load(f)
|
272
330
|
if "cells" in notebook and len(notebook["cells"]) > cell_index:
|
@@ -274,7 +332,7 @@ class FastAPINotebookBuilder:
|
|
274
332
|
else:
|
275
333
|
return None
|
276
334
|
|
277
|
-
def replace_cell_source(self, cell_index, new_source):
|
335
|
+
def replace_cell_source(self, cell_index: int, new_source: str | list[str]) -> None:
|
278
336
|
"""
|
279
337
|
Replace the source code of a specific Jupyter notebook cell.
|
280
338
|
|
@@ -283,6 +341,8 @@ class FastAPINotebookBuilder:
|
|
283
341
|
new_source (str): New source code to replace the cell's content.
|
284
342
|
"""
|
285
343
|
# Load the notebook
|
344
|
+
if not self.temp_notebook:
|
345
|
+
raise ValueError("No temporary notebook file path provided")
|
286
346
|
with open(self.temp_notebook, "r", encoding="utf-8") as f:
|
287
347
|
notebook = nbformat.read(f, as_version=4)
|
288
348
|
|
@@ -301,7 +361,7 @@ class FastAPINotebookBuilder:
|
|
301
361
|
print(f"Updated notebook saved to {self.temp_notebook}")
|
302
362
|
|
303
363
|
@staticmethod
|
304
|
-
def find_last_import_line(cell_source):
|
364
|
+
def find_last_import_line(cell_source: list[str]) -> int:
|
305
365
|
"""
|
306
366
|
Finds the index of the last line with an import statement in a list of code lines,
|
307
367
|
including multiline import statements.
|
@@ -339,7 +399,7 @@ class FastAPINotebookBuilder:
|
|
339
399
|
return last_import_index
|
340
400
|
|
341
401
|
@staticmethod
|
342
|
-
def extract_log_variables(cell):
|
402
|
+
def extract_log_variables(cell: dict) -> list[str]:
|
343
403
|
"""Extracts log variables from the first cell."""
|
344
404
|
if "source" in cell:
|
345
405
|
for line in cell["source"]:
|
@@ -355,7 +415,8 @@ class FastAPINotebookBuilder:
|
|
355
415
|
pass
|
356
416
|
return []
|
357
417
|
|
358
|
-
|
418
|
+
@staticmethod
|
419
|
+
def tag_questions(cells_dict: dict) -> dict:
|
359
420
|
"""
|
360
421
|
Adds 'is_first' and 'is_last' boolean flags to the cells based on their position
|
361
422
|
within the group of the same question. All cells will have both flags.
|
@@ -377,7 +438,7 @@ class FastAPINotebookBuilder:
|
|
377
438
|
raise KeyError(f"Cell {key} is missing the 'question' key.")
|
378
439
|
|
379
440
|
# Group the keys by question name
|
380
|
-
question_groups = {}
|
441
|
+
question_groups: dict = {}
|
381
442
|
for key, cell in cells_dict.items():
|
382
443
|
question = cell.get(
|
383
444
|
"question"
|
@@ -397,7 +458,9 @@ class FastAPINotebookBuilder:
|
|
397
458
|
|
398
459
|
return cells_dict
|
399
460
|
|
400
|
-
def question_dict(self):
|
461
|
+
def question_dict(self) -> dict:
|
462
|
+
if not self.temp_notebook:
|
463
|
+
raise ValueError("No temporary notebook file path provided")
|
401
464
|
notebook_path = Path(self.temp_notebook)
|
402
465
|
if not notebook_path.exists():
|
403
466
|
raise FileNotFoundError(f"The file {notebook_path} does not exist.")
|
@@ -406,15 +469,14 @@ class FastAPINotebookBuilder:
|
|
406
469
|
notebook = json.load(f)
|
407
470
|
|
408
471
|
results_dict = {}
|
472
|
+
question_name = None # At least define the variable up front
|
409
473
|
|
410
474
|
for cell_index, cell in enumerate(notebook.get("cells", [])):
|
411
475
|
if cell.get("cell_type") == "raw":
|
412
476
|
source = "".join(cell.get("source", ""))
|
413
477
|
if source.strip().startswith("# BEGIN QUESTION"):
|
414
|
-
|
415
|
-
question_name = (
|
416
|
-
question_name.group(1).strip() if question_name else None
|
417
|
-
)
|
478
|
+
name_match = re.search(r"name:\s*(.*)", source)
|
479
|
+
question_name = name_match.group(1).strip() if name_match else None
|
418
480
|
|
419
481
|
elif cell.get("cell_type") == "code":
|
420
482
|
source = "".join(cell.get("source", ""))
|
@@ -0,0 +1,190 @@
|
|
1
|
+
import argparse
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
|
5
|
+
from nbformat.v4 import new_markdown_cell, new_notebook
|
6
|
+
|
7
|
+
|
8
|
+
class QuestionCollator:
|
9
|
+
def __init__(self, root_folder: str, output_path: str):
|
10
|
+
"""
|
11
|
+
Initializes the QuestionCollator with the root folder and output path.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
root_folder (str): Path to the root folder containing the solution files.
|
15
|
+
output_path (str): Path to save the collated notebook.
|
16
|
+
"""
|
17
|
+
self.root_folder = root_folder
|
18
|
+
self.output_path = output_path
|
19
|
+
|
20
|
+
def find_solution_folders(self):
|
21
|
+
"""
|
22
|
+
Finds all immediate subdirectories inside '_solution*' folders that contain notebooks.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
list: List of folder paths containing notebooks.
|
26
|
+
"""
|
27
|
+
solution_folders = []
|
28
|
+
|
29
|
+
# Look for _solution* folders inside the root_folder
|
30
|
+
for dir_name in os.listdir(self.root_folder):
|
31
|
+
solution_folder_path = os.path.join(self.root_folder, dir_name)
|
32
|
+
|
33
|
+
if os.path.isdir(solution_folder_path) and dir_name.startswith("_solution"):
|
34
|
+
print(f"Found solution folder: {solution_folder_path}") # Debug output
|
35
|
+
|
36
|
+
# Now, look for immediate subdirectories inside this _solution* folder
|
37
|
+
for sub_dir in os.listdir(solution_folder_path):
|
38
|
+
sub_dir_path = os.path.join(solution_folder_path, sub_dir)
|
39
|
+
|
40
|
+
if os.path.isdir(sub_dir_path):
|
41
|
+
# Check if this subdirectory contains at least one .ipynb file
|
42
|
+
if any(f.endswith(".ipynb") for f in os.listdir(sub_dir_path)):
|
43
|
+
solution_folders.append(sub_dir_path)
|
44
|
+
|
45
|
+
print(f"Final list of solution subfolders: {solution_folders}") # Debug output
|
46
|
+
return solution_folders
|
47
|
+
|
48
|
+
def extract_questions(self, folder_path):
|
49
|
+
"""
|
50
|
+
Extracts questions from all notebooks in the solution folder.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
folder_path (str): Path to the solution folder.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
dict: Dictionary of categorized questions.
|
57
|
+
"""
|
58
|
+
questions = {
|
59
|
+
"multiple_choice": [],
|
60
|
+
"select_many": [],
|
61
|
+
"true_false": [],
|
62
|
+
"other": [],
|
63
|
+
}
|
64
|
+
|
65
|
+
for file in os.listdir(folder_path):
|
66
|
+
if file.endswith(".ipynb"):
|
67
|
+
file_path = os.path.join(folder_path, file)
|
68
|
+
print(
|
69
|
+
f"Processing notebook: {file_path}"
|
70
|
+
) # Print the full path of the notebook
|
71
|
+
with open(file_path, "r") as f:
|
72
|
+
content = json.load(f)
|
73
|
+
|
74
|
+
# Track whether we are inside a question block
|
75
|
+
in_question_block = False
|
76
|
+
current_question_content = []
|
77
|
+
|
78
|
+
for cell in content["cells"]:
|
79
|
+
if "# BEGIN MULTIPLE CHOICE" in cell["source"]:
|
80
|
+
# Start of a question block
|
81
|
+
in_question_block = True
|
82
|
+
current_question_content = []
|
83
|
+
elif "# END MULTIPLE CHOICE" in cell["source"]:
|
84
|
+
# End of a question block
|
85
|
+
in_question_block = False
|
86
|
+
if current_question_content:
|
87
|
+
questions["multiple_choice"].append(
|
88
|
+
{"source": "\n".join(current_question_content)}
|
89
|
+
)
|
90
|
+
current_question_content = []
|
91
|
+
elif in_question_block and cell["cell_type"] == "markdown":
|
92
|
+
# Capture markdown cells within the question block
|
93
|
+
current_question_content.append(cell["source"])
|
94
|
+
|
95
|
+
return questions
|
96
|
+
|
97
|
+
def create_collated_notebook(self, questions):
|
98
|
+
"""
|
99
|
+
Creates a new notebook with questions organized by type.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
questions (dict): Dictionary of categorized questions.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Notebook: The collated notebook.
|
106
|
+
"""
|
107
|
+
nb = new_notebook()
|
108
|
+
|
109
|
+
# Add Multiple Choice Questions
|
110
|
+
nb.cells.append(new_markdown_cell("# Multiple Choice Questions"))
|
111
|
+
for q in questions["multiple_choice"]:
|
112
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
113
|
+
|
114
|
+
# Add Select Many Questions
|
115
|
+
nb.cells.append(new_markdown_cell("# Select Many Questions"))
|
116
|
+
for q in questions["select_many"]:
|
117
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
118
|
+
|
119
|
+
# Add True/False Questions
|
120
|
+
nb.cells.append(new_markdown_cell("# True/False Questions"))
|
121
|
+
for q in questions["true_false"]:
|
122
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
123
|
+
|
124
|
+
# Add Other Questions
|
125
|
+
nb.cells.append(new_markdown_cell("# Other Questions"))
|
126
|
+
for q in questions["other"]:
|
127
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
128
|
+
|
129
|
+
return nb
|
130
|
+
|
131
|
+
def save_notebook(self, nb):
|
132
|
+
"""
|
133
|
+
Saves the collated notebook to the specified output path.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
nb (Notebook): The notebook to save.
|
137
|
+
"""
|
138
|
+
import nbformat
|
139
|
+
|
140
|
+
with open(self.output_path, "w") as f:
|
141
|
+
nbformat.write(nb, f)
|
142
|
+
|
143
|
+
def collate_questions(self):
|
144
|
+
"""
|
145
|
+
Collates questions from all solution folders and saves them to a new notebook.
|
146
|
+
"""
|
147
|
+
solution_folders = self.find_solution_folders()
|
148
|
+
all_questions = {
|
149
|
+
"multiple_choice": [],
|
150
|
+
"select_many": [],
|
151
|
+
"true_false": [],
|
152
|
+
"other": [],
|
153
|
+
}
|
154
|
+
|
155
|
+
for folder in solution_folders:
|
156
|
+
questions = self.extract_questions(folder)
|
157
|
+
all_questions["multiple_choice"].extend(questions["multiple_choice"])
|
158
|
+
all_questions["select_many"].extend(questions["select_many"])
|
159
|
+
all_questions["true_false"].extend(questions["true_false"])
|
160
|
+
all_questions["other"].extend(questions["other"])
|
161
|
+
|
162
|
+
collated_nb = self.create_collated_notebook(all_questions)
|
163
|
+
self.save_notebook(collated_nb)
|
164
|
+
print(f"Collated notebook saved to {self.output_path}")
|
165
|
+
|
166
|
+
|
167
|
+
def main():
|
168
|
+
parser = argparse.ArgumentParser(
|
169
|
+
description="Collate questions from solution folders into a single notebook."
|
170
|
+
)
|
171
|
+
parser.add_argument(
|
172
|
+
"root_folder",
|
173
|
+
type=str,
|
174
|
+
help="Path to the root folder containing solution folders",
|
175
|
+
)
|
176
|
+
parser.add_argument(
|
177
|
+
"output_path", type=str, help="Path to save the collated notebook"
|
178
|
+
)
|
179
|
+
|
180
|
+
args = parser.parse_args()
|
181
|
+
collator = QuestionCollator(
|
182
|
+
root_folder=args.root_folder, output_path=args.output_path
|
183
|
+
)
|
184
|
+
collator.collate_questions()
|
185
|
+
|
186
|
+
|
187
|
+
if __name__ == "__main__":
|
188
|
+
import sys
|
189
|
+
|
190
|
+
sys.exit(main())
|
@@ -1,9 +1,9 @@
|
|
1
|
+
import os
|
2
|
+
|
1
3
|
import pandas as pd
|
2
4
|
import requests
|
3
5
|
from requests.auth import HTTPBasicAuth
|
4
6
|
|
5
|
-
from ..build.passwords import password, user
|
6
|
-
|
7
7
|
|
8
8
|
def format_assignment_table(assignments):
|
9
9
|
# Create DataFrame
|
@@ -32,16 +32,21 @@ def format_assignment_table(assignments):
|
|
32
32
|
return df
|
33
33
|
|
34
34
|
|
35
|
-
def get_student_grades(
|
36
|
-
|
37
|
-
)
|
35
|
+
def get_student_grades(student_username):
|
36
|
+
# Get env variables here, in the function, rather than globally
|
37
|
+
api_base_url = os.getenv("DB_URL")
|
38
|
+
student_user = os.getenv("user_name_student")
|
39
|
+
student_pw = os.getenv("keys_student")
|
40
|
+
|
41
|
+
if not api_base_url or not student_user or not student_pw:
|
42
|
+
raise ValueError("Environment variables not set")
|
43
|
+
|
38
44
|
params = {"username": student_username}
|
39
45
|
res = requests.get(
|
40
46
|
url=api_base_url.rstrip("/") + "/student-grades-testing",
|
41
47
|
params=params,
|
42
|
-
auth=HTTPBasicAuth(
|
48
|
+
auth=HTTPBasicAuth(student_user, student_pw),
|
43
49
|
)
|
44
|
-
|
45
50
|
[assignments, sub] = res.json()
|
46
51
|
|
47
52
|
assignments_df = format_assignment_table(assignments)
|
@@ -69,103 +74,3 @@ def filter_assignments(df, max_week=None, exclude_types=None):
|
|
69
74
|
df = df[~df["assignment_type"].isin(exclude_types)]
|
70
75
|
|
71
76
|
return df
|
72
|
-
|
73
|
-
|
74
|
-
# import os
|
75
|
-
# import numpy as np
|
76
|
-
# import pandas as pd
|
77
|
-
# import socket
|
78
|
-
# import requests
|
79
|
-
# from IPython.core.interactiveshell import ExecutionInfo
|
80
|
-
# from requests import Response
|
81
|
-
# from requests.auth import HTTPBasicAuth
|
82
|
-
# from requests.exceptions import RequestException
|
83
|
-
# from pykubegrader.graders.late_assignments import calculate_late_submission
|
84
|
-
|
85
|
-
|
86
|
-
# api_base_url = os.getenv("DB_URL")
|
87
|
-
# student_user = "admin" # os.getenv("user_name_student")
|
88
|
-
# student_pw = "TrgpUuadm2PWtdgtC7Yt" # os.getenv("keys_student")
|
89
|
-
|
90
|
-
# from_hostname = socket.gethostname().removeprefix("jupyter-")
|
91
|
-
# from_env = os.getenv("JUPYTERHUB_USER")
|
92
|
-
# params = {"username": from_env}
|
93
|
-
|
94
|
-
# letteronly = lambda s: re.sub(r'[^a-zA-Z]', '', s)
|
95
|
-
# start_date='2025-01-06'
|
96
|
-
|
97
|
-
# # get assignment information
|
98
|
-
# res = requests.get(
|
99
|
-
# url=api_base_url.rstrip("/") + "/assignments",
|
100
|
-
# auth=HTTPBasicAuth(student_user, student_pw),)
|
101
|
-
# res.raise_for_status()
|
102
|
-
# assignments = res.json()
|
103
|
-
|
104
|
-
# # get submission information
|
105
|
-
# res = requests.get(
|
106
|
-
# url=api_base_url.rstrip("/") + "/testing/get-all-assignment-subs",
|
107
|
-
# auth=HTTPBasicAuth('testing', 'Vok8WzmuCMVYULw3tqzJ'),
|
108
|
-
# )
|
109
|
-
# subs = res.json()
|
110
|
-
# student_subs = [sub for sub in subs if sub['student_email']==from_env]
|
111
|
-
|
112
|
-
# # set up new df format
|
113
|
-
# weights = {'homework':0.15, 'lab':0.15, 'lecture':0.15, 'quiz':0.15, 'readings':0.15,
|
114
|
-
# # 'midterm':0.15, 'final':0.2
|
115
|
-
# 'labattendance':0.05, 'practicequiz':0.05, }
|
116
|
-
# assignment_types = list(set([a['assignment_type'] for a in assignments]))+['Running Avg']
|
117
|
-
# inds = [f'week{i+1}' for i in range(11)]+['Running Avg']
|
118
|
-
# restruct_grades = {k: np.zeros(len(inds)) for k in assignment_types}
|
119
|
-
# restruct_grades['inds']=inds
|
120
|
-
# new_weekly_grades = pd.DataFrame(restruct_grades)
|
121
|
-
|
122
|
-
# for assignment in assignments:
|
123
|
-
# # get the assignment from all submissions
|
124
|
-
# subs = [ sub for sub in student_subs if \
|
125
|
-
# letteronly(sub['assignment_type'])==letteronly(assignment['assignment_type']) and \
|
126
|
-
# sub['week_number']==assignment['week_number'] ]
|
127
|
-
# if len(subs)==0: continue
|
128
|
-
# if len(subs)>1:
|
129
|
-
|
130
|
-
# # get due date from assignment
|
131
|
-
# due_date = datetime.datetime.strptime(assignment['due_date'], "%Y-%m-%d %H:%M:%S")
|
132
|
-
# for sub in subs:
|
133
|
-
# entry_date = datetime.strptime(sub['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
|
134
|
-
# if entry_date <= due_date:
|
135
|
-
# else after_due).append(entry)
|
136
|
-
# calculate_late_submission(due = due_date, # '2025-01-21T18:59:59Z'.
|
137
|
-
# submitted = subs"%Y-%m-%d %H:%M:%S".
|
138
|
-
# - Q0 (float): Initial value (default is 100).
|
139
|
-
# - Q_min (float): Minimum value (default is 40).
|
140
|
-
# - k (float): Decay constant per minute (default is 6.88e-5).
|
141
|
-
|
142
|
-
# # get max from before due date
|
143
|
-
|
144
|
-
# # get max score from after due date and calculate
|
145
|
-
# print(sub['assignment'])
|
146
|
-
# print(subs)
|
147
|
-
# return
|
148
|
-
# # fill out grades
|
149
|
-
# new_weekly_grades.set_index('inds',inplace=True)
|
150
|
-
# splitted = [col_name.split('-')+[grades[col_name][0]] for col_name in grades.columns]
|
151
|
-
# for week,assignment,grade in splitted: new_weekly_grades.loc[week,assignment] = grade
|
152
|
-
|
153
|
-
# # Calculate the current week (1-based indexing)
|
154
|
-
# start_date = datetime.strptime(start_date, "%Y-%m-%d")
|
155
|
-
# today = datetime.now()
|
156
|
-
# days_since_start = (today - start_date).days
|
157
|
-
# current_week = days_since_start // 7 + 1
|
158
|
-
|
159
|
-
# # Get average until current week
|
160
|
-
# new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[:current_week-1].mean()
|
161
|
-
|
162
|
-
# # make new dataframe with the midterm, final, and running average
|
163
|
-
# max_key_length = max(len(k) for k in weights.keys())
|
164
|
-
# total = 0
|
165
|
-
# for k, v in weights.items():
|
166
|
-
# grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
|
167
|
-
# total+=grade*v
|
168
|
-
# print(f'{k:<{max_key_length}}:\t {grade:.2f}')
|
169
|
-
# print(f'\nTotal: {total}') # exclude midterm and final
|
170
|
-
|
171
|
-
# return new_out
|
@@ -1,5 +1,16 @@
|
|
1
1
|
{
|
2
2
|
"cells": [
|
3
|
+
{
|
4
|
+
"cell_type": "code",
|
5
|
+
"execution_count": null,
|
6
|
+
"metadata": {},
|
7
|
+
"outputs": [],
|
8
|
+
"source": [
|
9
|
+
"from grade_reports.grade_reports import filter_assignments, get_student_grades\n",
|
10
|
+
"\n",
|
11
|
+
"get_student_grades"
|
12
|
+
]
|
13
|
+
},
|
3
14
|
{
|
4
15
|
"cell_type": "code",
|
5
16
|
"execution_count": null,
|
@@ -19,13 +30,13 @@
|
|
19
30
|
"api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\"\n",
|
20
31
|
"\n",
|
21
32
|
"\n",
|
22
|
-
"def get_all_students():\n",
|
23
|
-
"
|
24
|
-
"
|
25
|
-
"
|
26
|
-
"
|
33
|
+
"# def get_all_students():\n",
|
34
|
+
"# res = requests.get(\n",
|
35
|
+
"# url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
|
36
|
+
"# auth=HTTPBasicAuth(user(), password()),\n",
|
37
|
+
"# )\n",
|
27
38
|
"\n",
|
28
|
-
"
|
39
|
+
"# return res.json()\n",
|
29
40
|
"\n",
|
30
41
|
"\n",
|
31
42
|
"# def get_student_grades(student_id):\n",
|
@@ -41,47 +52,47 @@
|
|
41
52
|
"# return assignments, sub\n",
|
42
53
|
"\n",
|
43
54
|
"\n",
|
44
|
-
"def get_student(student):\n",
|
45
|
-
"
|
46
|
-
"
|
47
|
-
"
|
55
|
+
"# def get_student(student):\n",
|
56
|
+
"# print(student)\n",
|
57
|
+
"# # Get assignments and submissions for the student (assumed functions)\n",
|
58
|
+
"# assignments, submissions = get_student_grades(student)\n",
|
48
59
|
"\n",
|
49
|
-
"
|
50
|
-
"
|
60
|
+
"# # Recalculate grades and get a grades dictionary\n",
|
61
|
+
"# grades_dict = recalculate_best_grades(assignments, submissions)\n",
|
51
62
|
"\n",
|
52
|
-
"
|
53
|
-
"
|
63
|
+
"# # Calculate averages and build a row for the student\n",
|
64
|
+
"# row = calculate_averages(grades_dict, student)\n",
|
54
65
|
"\n",
|
55
|
-
"
|
56
|
-
"
|
66
|
+
"# # Convert the row (a dictionary) into a DataFrame\n",
|
67
|
+
"# # row_df = pd.DataFrame([row])\n",
|
57
68
|
"\n",
|
58
|
-
"
|
69
|
+
"# return row\n",
|
59
70
|
"\n",
|
60
71
|
"\n",
|
61
|
-
"def get_all_student_grades():\n",
|
62
|
-
"
|
63
|
-
"
|
72
|
+
"# def get_all_student_grades():\n",
|
73
|
+
"# # Initialize an empty DataFrame to hold all student grades\n",
|
74
|
+
"# df = pd.DataFrame()\n",
|
64
75
|
"\n",
|
65
|
-
"
|
66
|
-
"
|
76
|
+
"# # Get all students (assuming get_all_students() is a defined function)\n",
|
77
|
+
"# students = get_all_students()\n",
|
67
78
|
"\n",
|
68
|
-
"
|
69
|
-
"
|
79
|
+
"# for student in students:\n",
|
80
|
+
"# row_df = get_student(student)\n",
|
70
81
|
"\n",
|
71
|
-
"
|
72
|
-
"
|
82
|
+
"# # Append the row to the DataFrame\n",
|
83
|
+
"# df = pd.concat([df, row_df], ignore_index=True)\n",
|
73
84
|
"\n",
|
74
|
-
"
|
85
|
+
"# return df\n",
|
75
86
|
"\n",
|
76
87
|
"\n",
|
77
|
-
"def get_max_deadline(assignments, assignment_name, week_number):\n",
|
78
|
-
"
|
79
|
-
"
|
80
|
-
"
|
81
|
-
"
|
88
|
+
"# def get_max_deadline(assignments, assignment_name, week_number):\n",
|
89
|
+
"# matching_rows = assignments[\n",
|
90
|
+
"# (assignments[\"week_number\"] == week_number)\n",
|
91
|
+
"# & (assignments[\"assignment_name\"] == assignment_name)\n",
|
92
|
+
"# ]\n",
|
82
93
|
"\n",
|
83
|
-
"
|
84
|
-
"
|
94
|
+
"# max_timestamp = matching_rows[\"due_date\"].max()\n",
|
95
|
+
"# return max_timestamp\n",
|
85
96
|
"\n",
|
86
97
|
"\n",
|
87
98
|
"def calculate_averages(grades_dict, student_id):\n",
|
@@ -455,7 +466,7 @@
|
|
455
466
|
],
|
456
467
|
"metadata": {
|
457
468
|
"kernelspec": {
|
458
|
-
"display_name": "
|
469
|
+
"display_name": "engr131w25",
|
459
470
|
"language": "python",
|
460
471
|
"name": "python3"
|
461
472
|
},
|
@@ -469,7 +480,7 @@
|
|
469
480
|
"name": "python",
|
470
481
|
"nbconvert_exporter": "python",
|
471
482
|
"pygments_lexer": "ipython3",
|
472
|
-
"version": "3.
|
483
|
+
"version": "3.13.1"
|
473
484
|
}
|
474
485
|
},
|
475
486
|
"nbformat": 4,
|
@@ -1,12 +1,7 @@
|
|
1
|
-
import asyncio
|
2
|
-
import base64
|
3
1
|
import os
|
4
2
|
|
5
|
-
import
|
6
|
-
|
7
|
-
|
8
|
-
# Apply nest_asyncio for environments like Jupyter
|
9
|
-
nest_asyncio.apply()
|
3
|
+
import requests
|
4
|
+
from requests.auth import HTTPBasicAuth
|
10
5
|
|
11
6
|
|
12
7
|
def get_credentials():
|
@@ -22,55 +17,52 @@ def get_credentials():
|
|
22
17
|
return {"username": username, "password": password}
|
23
18
|
|
24
19
|
|
25
|
-
|
20
|
+
def call_score_assignment(
|
26
21
|
assignment_title: str, notebook_title: str, file_path: str = ".output_reduced.log"
|
27
|
-
) -> dict:
|
22
|
+
) -> dict[str, str]:
|
28
23
|
"""
|
29
|
-
Submit an assignment to the scoring endpoint
|
24
|
+
Submit an assignment to the scoring endpoint
|
30
25
|
|
31
26
|
Args:
|
32
|
-
assignment_title (str): Title of the assignment
|
33
|
-
|
27
|
+
assignment_title (str): Title of the assignment
|
28
|
+
notebook_title (str): Title of the notebook
|
29
|
+
file_path (str): Path to the log file to upload
|
34
30
|
|
35
31
|
Returns:
|
36
|
-
dict: JSON response from the server
|
32
|
+
dict: JSON response from the server
|
37
33
|
"""
|
38
|
-
|
34
|
+
|
39
35
|
base_url = os.getenv("DB_URL")
|
40
36
|
if not base_url:
|
41
|
-
raise ValueError("Environment variable 'DB_URL'
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
except httpx.RequestError as e:
|
71
|
-
raise RuntimeError(f"An error occurred while requesting {url}: {e}")
|
72
|
-
except Exception as e:
|
73
|
-
raise RuntimeError(f"An unexpected error occurred: {e}")
|
37
|
+
raise ValueError("Environment variable 'DB_URL' not set")
|
38
|
+
|
39
|
+
url = base_url.rstrip("/") + "/score-assignment"
|
40
|
+
|
41
|
+
params = {
|
42
|
+
"assignment_title": assignment_title,
|
43
|
+
"notebook_title": notebook_title,
|
44
|
+
}
|
45
|
+
|
46
|
+
username, password = get_credentials().values()
|
47
|
+
|
48
|
+
try:
|
49
|
+
with open(file_path, "rb") as file:
|
50
|
+
res = requests.post(
|
51
|
+
url=url,
|
52
|
+
params=params,
|
53
|
+
auth=HTTPBasicAuth(username, password),
|
54
|
+
files={"log_file": file},
|
55
|
+
)
|
56
|
+
res.raise_for_status()
|
57
|
+
|
58
|
+
return res.json()
|
59
|
+
|
60
|
+
except FileNotFoundError:
|
61
|
+
raise FileNotFoundError(f"File {file_path} does not exist")
|
62
|
+
except requests.RequestException as err:
|
63
|
+
raise RuntimeError(f"An error occurred while requesting {url}: {err}")
|
64
|
+
except Exception as err:
|
65
|
+
raise RuntimeError(f"An unexpected error occurred: {err}")
|
74
66
|
|
75
67
|
|
76
68
|
def submit_assignment(
|
@@ -85,17 +77,9 @@ def submit_assignment(
|
|
85
77
|
assignment_title (str): Title of the assignment.
|
86
78
|
file_path (str): Path to the log file to upload.
|
87
79
|
"""
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
except RuntimeError:
|
92
|
-
loop = asyncio.new_event_loop()
|
93
|
-
asyncio.set_event_loop(loop)
|
94
|
-
|
95
|
-
# Run the async function in the event loop
|
96
|
-
response = loop.run_until_complete(
|
97
|
-
call_score_assignment(assignment_title, notebook_title, file_path)
|
98
|
-
)
|
80
|
+
|
81
|
+
response = call_score_assignment(assignment_title, notebook_title, file_path)
|
82
|
+
|
99
83
|
print("Server Response:", response.get("message", "No message in response"))
|
100
84
|
|
101
85
|
|
pykubegrader/telemetry.py
CHANGED
@@ -10,11 +10,13 @@ from typing import Any, Optional
|
|
10
10
|
import nacl.public
|
11
11
|
import pandas as pd
|
12
12
|
import requests
|
13
|
+
from dateutil import parser
|
13
14
|
from IPython.core.interactiveshell import ExecutionInfo
|
14
15
|
from requests import Response
|
15
16
|
from requests.auth import HTTPBasicAuth
|
16
17
|
from requests.exceptions import RequestException
|
17
18
|
|
19
|
+
from .graders.late_assignments import calculate_late_submission
|
18
20
|
from .utils import api_base_url, student_pw, student_user
|
19
21
|
|
20
22
|
#
|
@@ -206,10 +208,14 @@ def verify_server(jhub_user: Optional[str] = None) -> str:
|
|
206
208
|
return message
|
207
209
|
|
208
210
|
|
211
|
+
# TODO: reformat into a nice table
|
209
212
|
def get_my_grades() -> pd.DataFrame:
|
213
|
+
# get all submissions,
|
214
|
+
# recalculate late penalty in new columns,
|
215
|
+
# take max,
|
216
|
+
# divide by total points
|
210
217
|
if not student_user or not student_pw or not api_base_url:
|
211
218
|
raise ValueError("Necessary environment variables not set")
|
212
|
-
|
213
219
|
from_hostname = socket.gethostname().removeprefix("jupyter-")
|
214
220
|
from_env = os.getenv("JUPYTERHUB_USER")
|
215
221
|
if from_hostname != from_env:
|
@@ -283,3 +289,142 @@ def upload_execution_log() -> None:
|
|
283
289
|
res.raise_for_status()
|
284
290
|
|
285
291
|
print("Execution log uploaded successfully")
|
292
|
+
|
293
|
+
|
294
|
+
# #
|
295
|
+
# # Qiao's work on grades
|
296
|
+
#
|
297
|
+
|
298
|
+
|
299
|
+
def get_assignments_submissions():
|
300
|
+
if not student_user or not student_pw or not api_base_url:
|
301
|
+
raise ValueError("Necessary environment variables not set")
|
302
|
+
from_hostname = socket.gethostname().removeprefix("jupyter-")
|
303
|
+
from_env = os.getenv("JUPYTERHUB_USER")
|
304
|
+
if from_hostname != from_env:
|
305
|
+
raise ValueError("Problem with JupyterHub username")
|
306
|
+
|
307
|
+
params = {"username": from_env}
|
308
|
+
# get submission information
|
309
|
+
res = requests.get(
|
310
|
+
url=api_base_url.rstrip("/") + "/my-grades-testing",
|
311
|
+
params=params,
|
312
|
+
auth=HTTPBasicAuth(student_user, student_pw),
|
313
|
+
)
|
314
|
+
return res.json()
|
315
|
+
|
316
|
+
|
317
|
+
def setup_grades_df(assignments):
|
318
|
+
assignment_types = list(set([a["assignment_type"] for a in assignments]))
|
319
|
+
|
320
|
+
inds = [f"week{i + 1}" for i in range(11)] + ["Running Avg"]
|
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)
|
324
|
+
new_weekly_grades.set_index("inds", inplace=True)
|
325
|
+
return new_weekly_grades
|
326
|
+
|
327
|
+
|
328
|
+
def fill_grades_df(new_weekly_grades, assignments, student_subs):
|
329
|
+
for assignment in assignments:
|
330
|
+
# 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
|
+
]
|
337
|
+
if len(subs) == 0:
|
338
|
+
# print(assignment['title'], 0, assignment['max_score'])
|
339
|
+
continue
|
340
|
+
elif len(subs) == 1:
|
341
|
+
grade = subs[0]["raw_score"] / assignment["max_score"]
|
342
|
+
# print(assignment['title'], sub['raw_score'], assignment['max_score'])
|
343
|
+
else:
|
344
|
+
# get due date from assignment
|
345
|
+
due_date = parser.parse(assignment["due_date"])
|
346
|
+
grades = []
|
347
|
+
for sub in subs:
|
348
|
+
entry_date = parser.parse(sub["timestamp"])
|
349
|
+
if entry_date <= due_date:
|
350
|
+
grades.append(sub["raw_score"])
|
351
|
+
else:
|
352
|
+
grades.append(
|
353
|
+
calculate_late_submission(
|
354
|
+
due_date.strftime("%Y-%m-%d %H:%M:%S"),
|
355
|
+
entry_date.strftime("%Y-%m-%d %H:%M:%S"),
|
356
|
+
)
|
357
|
+
)
|
358
|
+
# print(assignment['title'], grades, assignment['max_score'])
|
359
|
+
grade = max(grades) / assignment["max_score"]
|
360
|
+
|
361
|
+
# fill out new df with max
|
362
|
+
new_weekly_grades.loc[
|
363
|
+
f"week{assignment['week_number']}", assignment["assignment_type"]
|
364
|
+
] = grade
|
365
|
+
|
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
|
381
|
+
|
382
|
+
|
383
|
+
def get_current_week(start_date):
|
384
|
+
# Calculate the current week (1-based indexing)
|
385
|
+
start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d")
|
386
|
+
today = datetime.datetime.now()
|
387
|
+
days_since_start = (today - start_date).days
|
388
|
+
return days_since_start // 7 + 1
|
389
|
+
|
390
|
+
|
391
|
+
# This function currently has many undefined variables and other problems!
|
392
|
+
def get_my_grades_testing(start_date="2025-01-06"):
|
393
|
+
"""takes in json.
|
394
|
+
reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
|
395
|
+
fills in 0 for missing assignments
|
396
|
+
calculate running average of each category"""
|
397
|
+
|
398
|
+
# set up new df format
|
399
|
+
weights = {
|
400
|
+
"homework": 0.15,
|
401
|
+
"lab": 0.15,
|
402
|
+
"lecture": 0.15,
|
403
|
+
"quiz": 0.15,
|
404
|
+
"readings": 0.15,
|
405
|
+
# 'midterm':0.15, 'final':0.2
|
406
|
+
"labattendance": 0.05,
|
407
|
+
"practicequiz": 0.05,
|
408
|
+
}
|
409
|
+
|
410
|
+
assignments, student_subs = get_assignments_submissions()
|
411
|
+
|
412
|
+
new_grades_df = setup_grades_df(assignments)
|
413
|
+
|
414
|
+
new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
|
415
|
+
|
416
|
+
current_week = get_current_week(start_date)
|
417
|
+
|
418
|
+
# Get average until current week
|
419
|
+
new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
|
420
|
+
|
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
|
429
|
+
|
430
|
+
return new_weekly_grades # get rid of test and running avg columns
|
@@ -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
|