PyKubeGrader 0.2.41__py3-none-any.whl → 0.3.1__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.2.41.dist-info → PyKubeGrader-0.3.1.dist-info}/METADATA +2 -1
- {PyKubeGrader-0.2.41.dist-info → PyKubeGrader-0.3.1.dist-info}/RECORD +16 -16
- pykubegrader/build/build_folder.py +78 -9
- pykubegrader/build/clean_folder.py +0 -1
- pykubegrader/build/markdown_questions.py +1 -0
- pykubegrader/telemetry.py +2 -2
- pykubegrader/utils.py +1 -3
- pykubegrader/widgets/multiple_choice.py +3 -2
- pykubegrader/widgets/question_processor.py +0 -1
- pykubegrader/widgets/select_many.py +16 -5
- pykubegrader/widgets_base/multi_select.py +3 -4
- pykubegrader/widgets_base/select.py +1 -1
- {PyKubeGrader-0.2.41.dist-info → PyKubeGrader-0.3.1.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.2.41.dist-info → PyKubeGrader-0.3.1.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.2.41.dist-info → PyKubeGrader-0.3.1.dist-info}/entry_points.txt +0 -0
- {PyKubeGrader-0.2.41.dist-info → PyKubeGrader-0.3.1.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
|
+
Version: 0.3.1
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -19,6 +19,7 @@ Requires-Dist: mypy
|
|
19
19
|
Requires-Dist: nbformat
|
20
20
|
Requires-Dist: nest_asyncio
|
21
21
|
Requires-Dist: numpy
|
22
|
+
Requires-Dist: pandas-stubs
|
22
23
|
Requires-Dist: panel
|
23
24
|
Requires-Dist: pynacl
|
24
25
|
Requires-Dist: pytest
|
@@ -1,13 +1,13 @@
|
|
1
1
|
pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
|
2
2
|
pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
|
3
|
-
pykubegrader/telemetry.py,sha256
|
4
|
-
pykubegrader/utils.py,sha256=
|
3
|
+
pykubegrader/telemetry.py,sha256=50Qp5WXeF7PD5FxDLFXWFAnQ2Yobj-wL3Dxh0Hz_vh0,6552
|
4
|
+
pykubegrader/utils.py,sha256=FrxuZ3gtTBTm5FQeH5c0bF9kFjA_AVtE5AYeFhzwKZ0,827
|
5
5
|
pykubegrader/validate.py,sha256=OKnItGyd-L8QPKcsE0KRuwBI_IxKiJzMLJKZiA2j3II,11184
|
6
6
|
pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
7
7
|
pykubegrader/build/api_notebook_builder.py,sha256=dlcVrGgsvxnt6GlAUN3e-FrpsPNJKXSHni1fstRCBik,20311
|
8
|
-
pykubegrader/build/build_folder.py,sha256=
|
9
|
-
pykubegrader/build/clean_folder.py,sha256=
|
10
|
-
pykubegrader/build/markdown_questions.py,sha256=
|
8
|
+
pykubegrader/build/build_folder.py,sha256=Ltd2skxEP1OdZA_D3Kb-WyTpDVtv4e35v6_XiMFZ9Vo,85378
|
9
|
+
pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
|
10
|
+
pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
|
11
11
|
pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
12
12
|
pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
|
13
13
|
pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
@@ -17,21 +17,21 @@ pykubegrader/submit/submit_assignment.py,sha256=UgJXKWw5b8-bRSFnba4iHAyXnujULHcW
|
|
17
17
|
pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
|
18
18
|
pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
|
19
19
|
pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
20
|
-
pykubegrader/widgets/multiple_choice.py,sha256=
|
21
|
-
pykubegrader/widgets/question_processor.py,sha256=
|
20
|
+
pykubegrader/widgets/multiple_choice.py,sha256=CE7y6DPIhm4UuD8I1nwWPF2l9sKtKvYhbyPpeZ1qmQc,2686
|
21
|
+
pykubegrader/widgets/question_processor.py,sha256=59R9oBiemuVJP0qzsR1kY8MeqDq4Kh99AYRq9RGujsg,1223
|
22
22
|
pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
|
23
|
-
pykubegrader/widgets/select_many.py,sha256=
|
23
|
+
pykubegrader/widgets/select_many.py,sha256=wyRPCEdI6GorPX3HzSbDiT_IHr34mFOUHzUN085-d88,4621
|
24
24
|
pykubegrader/widgets/student_info.py,sha256=xhQgKehk1r5e6N_hnjAIovLdPvQju6ZqQTOiPG0aevg,3568
|
25
25
|
pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs,1656
|
26
26
|
pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
|
27
27
|
pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
|
28
28
|
pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
29
|
-
pykubegrader/widgets_base/multi_select.py,sha256=
|
29
|
+
pykubegrader/widgets_base/multi_select.py,sha256=Btr09qjl2g2-wtKEtI3RYwo1Xm1dfFnHnDzw_1Yfqf4,4148
|
30
30
|
pykubegrader/widgets_base/reading.py,sha256=xmvN1UIXwk32v9S-JhsXwDc7axPlgpvoxSeM3II8sxY,5393
|
31
|
-
pykubegrader/widgets_base/select.py,sha256=
|
32
|
-
PyKubeGrader-0.
|
33
|
-
PyKubeGrader-0.
|
34
|
-
PyKubeGrader-0.
|
35
|
-
PyKubeGrader-0.
|
36
|
-
PyKubeGrader-0.
|
37
|
-
PyKubeGrader-0.
|
31
|
+
pykubegrader/widgets_base/select.py,sha256=RgSieRtBapAwXLd_ByJtT1L1EeUSi-9pFTuIm7zwDVE,2649
|
32
|
+
PyKubeGrader-0.3.1.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
|
33
|
+
PyKubeGrader-0.3.1.dist-info/METADATA,sha256=fqlGOEpvYY7AyGQV7ngPA01-D00cnsja365LanEQoFE,2806
|
34
|
+
PyKubeGrader-0.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
35
|
+
PyKubeGrader-0.3.1.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
|
36
|
+
PyKubeGrader-0.3.1.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
|
37
|
+
PyKubeGrader-0.3.1.dist-info/RECORD,,
|
@@ -616,6 +616,21 @@ class NotebookProcessor:
|
|
616
616
|
shutil.copy("./keys/.client_private_key.bin", client_private_key)
|
617
617
|
shutil.copy("./keys/.server_public_key.bin", server_public_key)
|
618
618
|
|
619
|
+
# Extract the assignment config
|
620
|
+
config = extract_config_from_notebook(temp_notebook_path)
|
621
|
+
|
622
|
+
files = extract_files(config)
|
623
|
+
|
624
|
+
# print(f"Files: {files}, from {temp_notebook_path}")
|
625
|
+
|
626
|
+
if files:
|
627
|
+
for file in files:
|
628
|
+
print(f"Copying {file} to {os.path.join(notebook_subfolder, file)}")
|
629
|
+
shutil.copy(
|
630
|
+
os.path.join(self.root_folder, file),
|
631
|
+
os.path.join(notebook_subfolder, file),
|
632
|
+
)
|
633
|
+
|
619
634
|
client_private_key = os.path.join(
|
620
635
|
notebook_subfolder,
|
621
636
|
".client_private_key.bin",
|
@@ -1352,15 +1367,18 @@ def extract_SELECT_MANY(ipynb_file):
|
|
1352
1367
|
1 # Increment subquestion number for each question
|
1353
1368
|
)
|
1354
1369
|
|
1355
|
-
# Extract question text (### heading)
|
1356
|
-
question_text_match = re.search(
|
1357
|
-
|
1358
|
-
)
|
1359
|
-
question_text = (
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
)
|
1370
|
+
# # Extract question text (### heading)
|
1371
|
+
# question_text_match = re.search(
|
1372
|
+
# r"^###\s*\*\*(.+)\*\*", markdown_content, re.MULTILINE
|
1373
|
+
# )
|
1374
|
+
# question_text = (
|
1375
|
+
# question_text_match.group(1).strip()
|
1376
|
+
# if question_text_match
|
1377
|
+
# else None
|
1378
|
+
# )
|
1379
|
+
|
1380
|
+
# Extract question text enable multiple lines
|
1381
|
+
question_text = extract_question(markdown_content)
|
1364
1382
|
|
1365
1383
|
# Extract OPTIONS (lines after #### options)
|
1366
1384
|
options_match = re.search(
|
@@ -2126,6 +2144,57 @@ def update_initialize_assignment(
|
|
2126
2144
|
print(f"No matching lines found in '{notebook_path}'.")
|
2127
2145
|
|
2128
2146
|
|
2147
|
+
def extract_config_from_notebook(notebook_path):
|
2148
|
+
"""
|
2149
|
+
Extract configuration text from a Jupyter Notebook.
|
2150
|
+
|
2151
|
+
Parameters:
|
2152
|
+
notebook_path (str): Path to the Jupyter Notebook file.
|
2153
|
+
|
2154
|
+
Returns:
|
2155
|
+
str: The configuration text if found, otherwise an empty string.
|
2156
|
+
"""
|
2157
|
+
with open(notebook_path, "r", encoding="utf-8") as f:
|
2158
|
+
notebook_data = json.load(f)
|
2159
|
+
|
2160
|
+
# Iterate through cells to find the configuration text
|
2161
|
+
config_text = ""
|
2162
|
+
for cell in notebook_data.get("cells", []):
|
2163
|
+
if cell.get("cell_type") == "raw": # Check for code cells
|
2164
|
+
source = "".join(cell.get("source", []))
|
2165
|
+
if "# ASSIGNMENT CONFIG" in source:
|
2166
|
+
config_text = source
|
2167
|
+
break
|
2168
|
+
|
2169
|
+
return config_text
|
2170
|
+
|
2171
|
+
|
2172
|
+
def extract_files(config_text):
|
2173
|
+
"""
|
2174
|
+
Extract the list of files from the given configuration text, excluding .bin files.
|
2175
|
+
|
2176
|
+
Parameters:
|
2177
|
+
config_text (str): The configuration text to process.
|
2178
|
+
|
2179
|
+
Returns:
|
2180
|
+
list: A list of file names excluding .bin files.
|
2181
|
+
"""
|
2182
|
+
# Regular expression to extract files list
|
2183
|
+
file_pattern = re.search(r"files:\s*\[(.*?)\]", config_text, re.DOTALL)
|
2184
|
+
|
2185
|
+
if file_pattern:
|
2186
|
+
files = file_pattern.group(1)
|
2187
|
+
# Split the list into individual file names and exclude .bin files
|
2188
|
+
file_list = [
|
2189
|
+
file.strip()
|
2190
|
+
for file in files.split(",")
|
2191
|
+
if not file.strip().endswith(".bin")
|
2192
|
+
]
|
2193
|
+
return file_list
|
2194
|
+
else:
|
2195
|
+
return []
|
2196
|
+
|
2197
|
+
|
2129
2198
|
def main():
|
2130
2199
|
parser = argparse.ArgumentParser(
|
2131
2200
|
description="Recursively process Jupyter notebooks with '# ASSIGNMENT CONFIG', move them to a solutions folder, and run otter assign."
|
pykubegrader/telemetry.py
CHANGED
@@ -5,9 +5,9 @@ import logging
|
|
5
5
|
import os
|
6
6
|
import socket
|
7
7
|
from typing import Any, Optional
|
8
|
-
import pandas as pd
|
9
8
|
|
10
9
|
import nacl.public
|
10
|
+
import pandas as pd
|
11
11
|
import requests
|
12
12
|
from IPython.core.interactiveshell import ExecutionInfo
|
13
13
|
from requests import Response
|
@@ -205,7 +205,7 @@ def verify_server(jhub_user: Optional[str] = None) -> str:
|
|
205
205
|
return message
|
206
206
|
|
207
207
|
|
208
|
-
def get_my_grades() ->
|
208
|
+
def get_my_grades() -> pd.DataFrame:
|
209
209
|
if not student_user or not student_pw or not api_base_url:
|
210
210
|
raise ValueError("Necessary environment variables not set")
|
211
211
|
|
pykubegrader/utils.py
CHANGED
@@ -15,9 +15,7 @@ def list_of_lists(options: list) -> bool:
|
|
15
15
|
|
16
16
|
def shuffle_options(options: list[Optional[str]], seed: int) -> list[Optional[str]]:
|
17
17
|
random.seed(seed)
|
18
|
-
|
19
|
-
for inner_list in options:
|
20
|
-
random.shuffle(inner_list)
|
18
|
+
random.shuffle(options)
|
21
19
|
|
22
20
|
return options
|
23
21
|
|
@@ -16,7 +16,6 @@ def MCQ(
|
|
16
16
|
options: list[str] | list[list[str]],
|
17
17
|
initial_vals: list[str],
|
18
18
|
) -> Tuple[list[pn.pane.HTML], list[pn.widgets.RadioButtonGroup]]:
|
19
|
-
|
20
19
|
# Process descriptions through `process_questions_and_codes`
|
21
20
|
processed_titles, code_blocks = process_questions_and_codes(descriptions)
|
22
21
|
|
@@ -29,7 +28,9 @@ def MCQ(
|
|
29
28
|
)
|
30
29
|
# Add the title and code block in a row
|
31
30
|
if code_block:
|
32
|
-
desc_widgets.append(
|
31
|
+
desc_widgets.append(
|
32
|
+
pn.Column(title_pane, code_block, sizing_mode="stretch_width")
|
33
|
+
)
|
33
34
|
else:
|
34
35
|
desc_widgets.append(pn.Column(title_pane, sizing_mode="stretch_width"))
|
35
36
|
|
@@ -2,6 +2,7 @@ import panel as pn
|
|
2
2
|
|
3
3
|
from ..widgets.style import drexel_colors, raw_css
|
4
4
|
from ..widgets_base.multi_select import MultiSelectQuestion
|
5
|
+
from .question_processor import process_questions_and_codes
|
5
6
|
|
6
7
|
# Pass the custom CSS to Panel
|
7
8
|
pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
|
@@ -24,15 +25,25 @@ def MultiSelect(
|
|
24
25
|
separator = pn.pane.HTML("<hr style='border:1px solid lightgray; width:100%;'>")
|
25
26
|
|
26
27
|
i = 0
|
28
|
+
desc_width = "500px"
|
27
29
|
|
28
30
|
for question, option_set in zip(descriptions, options):
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
# Process descriptions through `process_questions_and_codes`
|
32
|
+
processed_titles, code_blocks = process_questions_and_codes(question)
|
33
|
+
|
34
|
+
# Create an HTML pane for the title
|
35
|
+
title_pane = pn.pane.HTML(
|
33
36
|
f"<hr style='border:1px solid lightgray; width:100%;'>"
|
34
|
-
f"<div style='text-align: left; width: {desc_width};'><b>{
|
37
|
+
f"<div style='text-align: left; width: {desc_width};'><b>{processed_titles[0]}</b></div>"
|
35
38
|
)
|
39
|
+
# Add the title and code block in a row
|
40
|
+
if code_blocks[0]:
|
41
|
+
desc_widget = pn.Column(title_pane, code_blocks[0], sizing_mode="stretch_width")
|
42
|
+
else:
|
43
|
+
desc_widget = title_pane
|
44
|
+
|
45
|
+
# # Create description widget with separator
|
46
|
+
|
36
47
|
|
37
48
|
# Create checkboxes for current question
|
38
49
|
checkbox_set = [
|
@@ -4,7 +4,7 @@ from typing import Callable, Tuple
|
|
4
4
|
import panel as pn
|
5
5
|
|
6
6
|
from ..telemetry import ensure_responses, score_question, update_responses
|
7
|
-
from ..utils import shuffle_questions
|
7
|
+
from ..utils import shuffle_questions
|
8
8
|
from ..widgets.style import drexel_colors, raw_css
|
9
9
|
|
10
10
|
# Pass the custom CSS to Panel
|
@@ -58,7 +58,7 @@ class MultiSelectQuestion:
|
|
58
58
|
|
59
59
|
# # add shuffle options to multi_select.py
|
60
60
|
# options = shuffle_options(options, seed)
|
61
|
-
|
61
|
+
|
62
62
|
description_widgets, self.widgets = style(
|
63
63
|
descriptions, options, self.initial_vals
|
64
64
|
)
|
@@ -72,14 +72,13 @@ class MultiSelectQuestion:
|
|
72
72
|
question_header = pn.pane.HTML(
|
73
73
|
f"<h2>Question {self.question_number}: {title}</h2>"
|
74
74
|
)
|
75
|
-
|
75
|
+
|
76
76
|
question_body = pn.Column(
|
77
77
|
*[
|
78
78
|
pn.Row(desc_widget, checkbox_set)
|
79
79
|
for desc_widget, checkbox_set in widget_pairs
|
80
80
|
]
|
81
81
|
)
|
82
|
-
|
83
82
|
|
84
83
|
self.layout = pn.Column(question_header, question_body, self.submit_button)
|
85
84
|
|
@@ -4,7 +4,7 @@ from typing import Callable, Tuple
|
|
4
4
|
import panel as pn
|
5
5
|
|
6
6
|
from ..telemetry import ensure_responses, score_question, update_responses
|
7
|
-
from ..utils import shuffle_questions
|
7
|
+
from ..utils import shuffle_questions
|
8
8
|
from ..widgets.style import drexel_colors
|
9
9
|
|
10
10
|
# Pass custom CSS to Panel
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|