PyKubeGrader 0.1.10__tar.gz → 0.1.12__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pykubegrader-0.1.10/src/PyKubeGrader.egg-info → pykubegrader-0.1.12}/PKG-INFO +1 -1
- {pykubegrader-0.1.10 → pykubegrader-0.1.12/src/PyKubeGrader.egg-info}/PKG-INFO +1 -1
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/build/api_notebook_builder.py +10 -10
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/build/build_folder.py +57 -30
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/initialize.py +11 -14
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/telemetry.py +3 -4
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/validate.py +2 -2
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/.coveragerc +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/.gitignore +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/.readthedocs.yml +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/AUTHORS.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/CHANGELOG.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/LICENSE.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/README.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/Makefile +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/_static/custom.css +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/authors.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/changelog.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/conf.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/contributing.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/index.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/license.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/readme.rst +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/docs/requirements.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/examples/.responses.json +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/examples/true_false.ipynb +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/pyproject.toml +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/setup.cfg +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/setup.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/SOURCES.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/requires.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/__init__.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/select_many.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/style.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/true_false.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets_base/multi_select.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets_base/reading.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/src/pykubegrader/widgets_base/select.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/tests/conftest.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/tests/import_test.py +0 -0
- {pykubegrader-0.1.10 → pykubegrader-0.1.12}/tox.ini +0 -0
@@ -6,6 +6,7 @@ import nbformat
|
|
6
6
|
import json
|
7
7
|
import re
|
8
8
|
import shutil
|
9
|
+
import ast
|
9
10
|
|
10
11
|
|
11
12
|
@dataclass
|
@@ -20,7 +21,6 @@ class FastAPINotebookBuilder:
|
|
20
21
|
self.run()
|
21
22
|
|
22
23
|
def run(self):
|
23
|
-
|
24
24
|
# here for easy debugging
|
25
25
|
if self.temp_notebook is not None:
|
26
26
|
shutil.copy(
|
@@ -34,7 +34,6 @@ class FastAPINotebookBuilder:
|
|
34
34
|
self.add_api_code()
|
35
35
|
|
36
36
|
def add_api_code(self):
|
37
|
-
|
38
37
|
for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
|
39
38
|
print(
|
40
39
|
f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
|
@@ -105,7 +104,6 @@ class FastAPINotebookBuilder:
|
|
105
104
|
@staticmethod
|
106
105
|
def construct_update_responses(cell_dict):
|
107
106
|
update_responses = []
|
108
|
-
question_id = cell_dict["question"] + "-" + str(cell_dict["test_number"]) + "\n"
|
109
107
|
|
110
108
|
logging_variables = cell_dict["logging_variables"]
|
111
109
|
|
@@ -141,7 +139,6 @@ class FastAPINotebookBuilder:
|
|
141
139
|
|
142
140
|
@staticmethod
|
143
141
|
def construct_graders(cell_dict):
|
144
|
-
|
145
142
|
# Generate Python code
|
146
143
|
added_code = [
|
147
144
|
"if "
|
@@ -283,7 +280,7 @@ class FastAPINotebookBuilder:
|
|
283
280
|
"""
|
284
281
|
last_import_index = -1
|
285
282
|
is_multiline_import = False # Flag to track if we're inside a multiline import
|
286
|
-
|
283
|
+
|
287
284
|
for i, line in enumerate(cell_source):
|
288
285
|
stripped_line = line.strip()
|
289
286
|
|
@@ -313,11 +310,15 @@ class FastAPINotebookBuilder:
|
|
313
310
|
if "source" in cell:
|
314
311
|
for line in cell["source"]:
|
315
312
|
# Look for the log_variables pattern
|
316
|
-
match = re.search(r"log_variables:\s
|
313
|
+
match = re.search(r"log_variables:\s*(\[.*\])", line)
|
317
314
|
if match:
|
318
|
-
#
|
319
|
-
|
320
|
-
|
315
|
+
# Parse the list using ast.literal_eval for safety
|
316
|
+
try:
|
317
|
+
log_variables = ast.literal_eval(match.group(1))
|
318
|
+
if isinstance(log_variables, list):
|
319
|
+
return [var.strip() for var in log_variables]
|
320
|
+
except (SyntaxError, ValueError):
|
321
|
+
pass
|
321
322
|
return []
|
322
323
|
|
323
324
|
def tag_questions(cells_dict):
|
@@ -363,7 +364,6 @@ class FastAPINotebookBuilder:
|
|
363
364
|
return cells_dict
|
364
365
|
|
365
366
|
def question_dict(self):
|
366
|
-
|
367
367
|
notebook_path = Path(self.temp_notebook)
|
368
368
|
if not notebook_path.exists():
|
369
369
|
raise FileNotFoundError(f"The file {notebook_path} does not exist.")
|
@@ -166,7 +166,7 @@ class NotebookProcessor:
|
|
166
166
|
Returns:
|
167
167
|
None
|
168
168
|
"""
|
169
|
-
|
169
|
+
|
170
170
|
print(f"Processing notebook: {notebook_path}")
|
171
171
|
|
172
172
|
logging.info(f"Processing notebook: {notebook_path}")
|
@@ -278,26 +278,38 @@ class NotebookProcessor:
|
|
278
278
|
self, temp_notebook_path, notebook_subfolder, notebook_name
|
279
279
|
):
|
280
280
|
if self.has_assignment(temp_notebook_path, "# ASSIGNMENT CONFIG"):
|
281
|
-
|
282
281
|
# TODO: This is hardcoded for now, but should be in a configuration file.
|
283
282
|
client_private_key = os.path.join(
|
284
283
|
notebook_subfolder,
|
285
|
-
"client_private_key.bin",
|
284
|
+
".client_private_key.bin",
|
286
285
|
)
|
287
286
|
server_public_key = os.path.join(
|
288
287
|
notebook_subfolder,
|
289
|
-
"server_public_key.bin",
|
288
|
+
".server_public_key.bin",
|
290
289
|
)
|
291
290
|
|
292
|
-
shutil.copy("./keys
|
293
|
-
shutil.copy("./keys
|
291
|
+
shutil.copy("./keys/.client_private_key.bin", client_private_key)
|
292
|
+
shutil.copy("./keys/.server_public_key.bin", server_public_key)
|
294
293
|
|
295
294
|
FastAPINotebookBuilder(notebook_path=temp_notebook_path)
|
296
295
|
|
296
|
+
debug_notebook = os.path.join(
|
297
|
+
notebook_subfolder,
|
298
|
+
"dist",
|
299
|
+
"autograder",
|
300
|
+
os.path.basename(temp_notebook_path).replace("_temp", "_debugger"),
|
301
|
+
)
|
302
|
+
|
297
303
|
self.run_otter_assign(
|
298
304
|
temp_notebook_path, os.path.join(notebook_subfolder, "dist")
|
299
305
|
)
|
300
306
|
|
307
|
+
print(f"Copying {temp_notebook_path} to {debug_notebook}")
|
308
|
+
|
309
|
+
shutil.copy(temp_notebook_path, debug_notebook)
|
310
|
+
|
311
|
+
NotebookProcessor.remove_assignment_config_cells(debug_notebook)
|
312
|
+
|
301
313
|
student_notebook = os.path.join(
|
302
314
|
notebook_subfolder, "dist", "student", f"{notebook_name}.ipynb"
|
303
315
|
)
|
@@ -329,18 +341,34 @@ class NotebookProcessor:
|
|
329
341
|
NotebookProcessor.add_initialization_code(temp_notebook_path)
|
330
342
|
return None
|
331
343
|
|
344
|
+
@staticmethod
|
345
|
+
def remove_assignment_config_cells(notebook_path):
|
346
|
+
# Read the notebook
|
347
|
+
with open(notebook_path, "r", encoding="utf-8") as f:
|
348
|
+
notebook = nbformat.read(f, as_version=nbformat.NO_CONVERT)
|
349
|
+
|
350
|
+
# Filter out cells containing "# ASSIGNMENT CONFIG"
|
351
|
+
notebook.cells = [
|
352
|
+
cell
|
353
|
+
for cell in notebook.cells
|
354
|
+
if "# ASSIGNMENT CONFIG" not in cell.get("source", "")
|
355
|
+
]
|
356
|
+
|
357
|
+
# Save the updated notebook
|
358
|
+
with open(notebook_path, "w", encoding="utf-8") as f:
|
359
|
+
nbformat.write(notebook, f)
|
360
|
+
|
332
361
|
@staticmethod
|
333
362
|
def add_initialization_code(notebook_path):
|
334
363
|
# finds the first code cell
|
335
364
|
index, cell = find_first_code_cell(notebook_path)
|
336
|
-
cell = cell[
|
365
|
+
cell = cell["source"]
|
337
366
|
import_text = "from pykubegrader.initialize import initialize_assignment\n"
|
338
367
|
cell = f"{import_text}\n" + cell
|
339
368
|
cell += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}")\n'
|
340
369
|
replace_cell_source(notebook_path, index, cell)
|
341
370
|
|
342
371
|
def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
|
343
|
-
|
344
372
|
### Parse the notebook for multiple choice questions
|
345
373
|
if self.has_assignment(temp_notebook_path, "# BEGIN MULTIPLE CHOICE"):
|
346
374
|
self._print_and_log(
|
@@ -627,7 +655,7 @@ class NotebookProcessor:
|
|
627
655
|
"""
|
628
656
|
|
629
657
|
solutions = {}
|
630
|
-
total_points =
|
658
|
+
total_points = []
|
631
659
|
|
632
660
|
# If the output file exists, load the existing solutions and total_points
|
633
661
|
if os.path.exists(output_file):
|
@@ -641,14 +669,14 @@ class NotebookProcessor:
|
|
641
669
|
if hasattr(existing_module, "solutions"):
|
642
670
|
solutions.update(existing_module.solutions)
|
643
671
|
if hasattr(existing_module, "total_points"):
|
644
|
-
total_points
|
672
|
+
total_points.extend(existing_module.total_points)
|
645
673
|
|
646
674
|
# Process new question data and update solutions and total_points
|
647
675
|
for question_set in data_list:
|
648
676
|
for key, question_data in question_set.items():
|
649
677
|
solution_key = f"q{question_data['question number']}-{question_data['subquestion_number']}-{key}"
|
650
678
|
solutions[solution_key] = question_data["solution"]
|
651
|
-
total_points
|
679
|
+
total_points.extend([question_data["points"]])
|
652
680
|
|
653
681
|
# Write updated total_points and solutions back to the file
|
654
682
|
with open(output_file, "w", encoding="utf-8") as f:
|
@@ -1577,29 +1605,28 @@ def find_first_code_cell(notebook_path):
|
|
1577
1605
|
|
1578
1606
|
|
1579
1607
|
def replace_cell_source(notebook_path, cell_index, new_source):
|
1580
|
-
|
1581
|
-
|
1608
|
+
"""
|
1609
|
+
Replace the source code of a specific Jupyter notebook cell.
|
1582
1610
|
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1611
|
+
Args:
|
1612
|
+
cell_index (int): Index of the cell to be modified (0-based).
|
1613
|
+
new_source (str): New source code to replace the cell's content.
|
1614
|
+
"""
|
1615
|
+
# Load the notebook
|
1616
|
+
with open(notebook_path, "r", encoding="utf-8") as f:
|
1617
|
+
notebook = nbformat.read(f, as_version=4)
|
1590
1618
|
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
f"Cell index {cell_index} is out of range for this notebook."
|
1595
|
-
)
|
1619
|
+
# Check if the cell index is valid
|
1620
|
+
if cell_index >= len(notebook.cells) or cell_index < 0:
|
1621
|
+
raise IndexError(f"Cell index {cell_index} is out of range for this notebook.")
|
1596
1622
|
|
1597
|
-
|
1598
|
-
|
1623
|
+
# Replace the source code of the specified cell
|
1624
|
+
notebook.cells[cell_index]["source"] = new_source
|
1625
|
+
|
1626
|
+
# Save the notebook
|
1627
|
+
with open(notebook_path, "w", encoding="utf-8") as f:
|
1628
|
+
nbformat.write(notebook, f)
|
1599
1629
|
|
1600
|
-
# Save the notebook
|
1601
|
-
with open(notebook_path, "w", encoding="utf-8") as f:
|
1602
|
-
nbformat.write(notebook, f)
|
1603
1630
|
|
1604
1631
|
def main():
|
1605
1632
|
parser = argparse.ArgumentParser(
|
@@ -8,11 +8,11 @@ import requests
|
|
8
8
|
from .telemetry import telemetry, update_responses, ensure_responses
|
9
9
|
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
11
|
+
def initialize_assignment(
|
12
|
+
name: str,
|
13
|
+
verbose: Optional[bool] = False,
|
14
|
+
url: Optional[str] = "https://engr-131-api.eastus.cloudapp.azure.com/",
|
15
|
+
) -> None:
|
16
16
|
ipython = get_ipython()
|
17
17
|
if ipython is None:
|
18
18
|
print("Setup unsuccessful. Are you in a Jupyter environment?")
|
@@ -38,13 +38,12 @@ def initialize_assignment(name: str,
|
|
38
38
|
except (TypeError, json.JSONDecodeError) as e:
|
39
39
|
print(f"Failed to initialize assignment: {e}")
|
40
40
|
return
|
41
|
-
|
42
|
-
|
41
|
+
|
43
42
|
# extract responses
|
44
43
|
responses = ensure_responses()
|
45
|
-
|
46
|
-
# TODO: Add more checks here??
|
47
|
-
assert isinstance(responses.get(
|
44
|
+
|
45
|
+
# TODO: Add more checks here??
|
46
|
+
assert isinstance(responses.get("seed"), int), "valid seed not found in responses"
|
48
47
|
|
49
48
|
pn.extension(silent=True)
|
50
49
|
|
@@ -52,17 +51,15 @@ def initialize_assignment(name: str,
|
|
52
51
|
print("Assignment successfully initialized")
|
53
52
|
print(f"Assignment: {name}")
|
54
53
|
print(f"Username: {jhub_user}")
|
55
|
-
|
56
54
|
|
57
|
-
|
58
55
|
# Checks connectivity to the API
|
59
|
-
params = {
|
56
|
+
params = {"jhub_user": responses["jhub_user"]}
|
60
57
|
response = requests.get(url, params=params)
|
61
58
|
if verbose:
|
62
59
|
print(f"status code: {response.status_code}")
|
63
60
|
data = response.json()
|
64
61
|
for k, v in data.items():
|
65
62
|
print(f"{k}: {v}")
|
66
|
-
|
63
|
+
|
67
64
|
print("Assignment successfully initialized")
|
68
65
|
return responses
|
@@ -20,11 +20,11 @@ logging.basicConfig(filename=".output.log", level=logging.INFO, force=True)
|
|
20
20
|
|
21
21
|
|
22
22
|
def encrypt_to_b64(message: str) -> str:
|
23
|
-
with open("server_public_key.bin", "rb") as f:
|
23
|
+
with open(".server_public_key.bin", "rb") as f:
|
24
24
|
server_pub_key_bytes = f.read()
|
25
25
|
server_pub_key = nacl.public.PublicKey(server_pub_key_bytes)
|
26
26
|
|
27
|
-
with open("client_private_key.bin", "rb") as f:
|
27
|
+
with open(".client_private_key.bin", "rb") as f:
|
28
28
|
client_private_key_bytes = f.read()
|
29
29
|
client_priv_key = nacl.public.PrivateKey(client_private_key_bytes)
|
30
30
|
|
@@ -36,7 +36,6 @@ def encrypt_to_b64(message: str) -> str:
|
|
36
36
|
|
37
37
|
|
38
38
|
def ensure_responses() -> dict:
|
39
|
-
|
40
39
|
with open(".responses.json", "a") as _:
|
41
40
|
pass
|
42
41
|
|
@@ -48,7 +47,7 @@ def ensure_responses() -> dict:
|
|
48
47
|
except json.JSONDecodeError:
|
49
48
|
with open(".responses.json", "w") as f:
|
50
49
|
json.dump(responses, f)
|
51
|
-
|
50
|
+
|
52
51
|
return responses
|
53
52
|
|
54
53
|
|
@@ -252,11 +252,11 @@ def validate_logfile(
|
|
252
252
|
|
253
253
|
|
254
254
|
def generate_keys() -> nacl.public.Box:
|
255
|
-
with open("server_private_key.bin", "rb") as priv_file:
|
255
|
+
with open(".server_private_key.bin", "rb") as priv_file:
|
256
256
|
server_private_key_bytes = priv_file.read()
|
257
257
|
server_priv_key = nacl.public.PrivateKey(server_private_key_bytes)
|
258
258
|
|
259
|
-
with open("client_public_key.bin", "rb") as pub_file:
|
259
|
+
with open(".client_public_key.bin", "rb") as pub_file:
|
260
260
|
client_public_key_bytes = pub_file.read()
|
261
261
|
client_pub_key = nacl.public.PublicKey(client_public_key_bytes)
|
262
262
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|