PyKubeGrader 0.2.37__py3-none-any.whl → 0.2.39__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyKubeGrader
3
- Version: 0.2.37
3
+ Version: 0.2.39
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,12 +1,13 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
2
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
3
3
  pykubegrader/telemetry.py,sha256=-XiWKMlwneS4zD8-hPcMVZFY4b6gK4jHKTwhjUBw8gw,6556
4
- pykubegrader/utils.py,sha256=FrxuZ3gtTBTm5FQeH5c0bF9kFjA_AVtE5AYeFhzwKZ0,827
4
+ pykubegrader/utils.py,sha256=eqQSf2xOAtqB9pWSf6RI-WwwdEowVe8yr2XIiNpc5Rc,870
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=5jl_TAnxfd9sqWvggJr4i3E7mrEf03cmFDotVrgTZWQ,82697
8
+ pykubegrader/build/build_folder.py,sha256=WTdPFsV1PRJ9U_730ckcWezQfMEL2oCH4ZDoflmr30M,83138
9
9
  pykubegrader/build/clean_folder.py,sha256=dfs9NuZ-EP6q_xYQnZKH74aEafEB6hpTAZcSkY14UWI,1328
10
+ pykubegrader/build/markdown_questions.py,sha256=OEEFoM5L4Ptax0dm8Tz-gM01akJEoDvsPNgixE1Vk1s,4675
10
11
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
12
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
12
13
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -16,20 +17,21 @@ pykubegrader/submit/submit_assignment.py,sha256=UgJXKWw5b8-bRSFnba4iHAyXnujULHcW
16
17
  pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
17
18
  pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
18
19
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
- pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
20
+ pykubegrader/widgets/multiple_choice.py,sha256=oH1NjL2cuQDhn-9Ds8yBNAWDHr2anGS__F8uel7IsiI,2657
21
+ pykubegrader/widgets/question_processor.py,sha256=9mqyZVAhR65vYgGHVQZ4BVk7ksGBhNX-P3yf2Su43gI,1064
20
22
  pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
21
23
  pykubegrader/widgets/select_many.py,sha256=l7YQ8QT5k71j36KC1f5LmKIAX2bXpvMDGc6nqIJ1PeQ,4116
22
24
  pykubegrader/widgets/student_info.py,sha256=xhQgKehk1r5e6N_hnjAIovLdPvQju6ZqQTOiPG0aevg,3568
23
25
  pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs,1656
24
- pykubegrader/widgets/true_false.py,sha256=D45bjRLaAcNzsSlWPgxwTXGVZPE7PER34S30V6PjEXU,2807
26
+ pykubegrader/widgets/true_false.py,sha256=QllIhHuJstJft_RuShkxI_fFFTaDAlzNZOFNs00HLIM,2842
25
27
  pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
26
28
  pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
27
- pykubegrader/widgets_base/multi_select.py,sha256=Cl0IN21wXLZuFu-zC65aS9tD4jMfzCRJ2DPjHao5_Ak,4044
29
+ pykubegrader/widgets_base/multi_select.py,sha256=nSK_2hDCG67Nbf00nJfabcahNnNWt8W_4Pec8tg4xAM,4182
28
30
  pykubegrader/widgets_base/reading.py,sha256=xmvN1UIXwk32v9S-JhsXwDc7axPlgpvoxSeM3II8sxY,5393
29
- pykubegrader/widgets_base/select.py,sha256=Fw3uFNOIWo1a3CvlzSx23bvi6bSmA3TqutuRbhD4Dp8,2525
30
- PyKubeGrader-0.2.37.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
31
- PyKubeGrader-0.2.37.dist-info/METADATA,sha256=r05PaIWX6hTV2O04ezd46J0HI5hmutQoW0kgHARq1E8,2779
32
- PyKubeGrader-0.2.37.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
33
- PyKubeGrader-0.2.37.dist-info/entry_points.txt,sha256=UPMdTT46fQwTYJWtrUwIWIbXbwyOPfNQgBFRa0frWzw,138
34
- PyKubeGrader-0.2.37.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
35
- PyKubeGrader-0.2.37.dist-info/RECORD,,
31
+ pykubegrader/widgets_base/select.py,sha256=PJhBRDpAYkFnmDzvm7jxiGW1E5VcEtZBe_wEtDKs-ZQ,2662
32
+ PyKubeGrader-0.2.39.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
33
+ PyKubeGrader-0.2.39.dist-info/METADATA,sha256=F69tjll_smvwuljrJ9dK4_0CFP6ECHjGORZygayRiYw,2779
34
+ PyKubeGrader-0.2.39.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
35
+ PyKubeGrader-0.2.39.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
36
+ PyKubeGrader-0.2.39.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
37
+ PyKubeGrader-0.2.39.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
+ markdown-question = pykubegrader.build.markdown_questions:main
2
3
  otter-folder-builder = pykubegrader.build.build_folder:main
3
4
  otter-folder-cleaner = pykubegrader.build.clean_folder:main
@@ -1498,6 +1498,15 @@ def extract_TF(ipynb_file):
1498
1498
  return []
1499
1499
 
1500
1500
 
1501
+ def extract_question(text):
1502
+ # Regular expression to capture the multiline title
1503
+ match = re.search(r"###\s+(.*?)\s+####", text, re.DOTALL)
1504
+ if match:
1505
+ # Stripping unnecessary whitespace and asterisks
1506
+ return match.group(1).strip().strip("**")
1507
+ return None
1508
+
1509
+
1501
1510
  def extract_MCQ(ipynb_file):
1502
1511
  """
1503
1512
  Extracts multiple-choice questions from markdown cells within sections marked by
@@ -1552,15 +1561,18 @@ def extract_MCQ(ipynb_file):
1552
1561
  1 # Increment subquestion number for each question
1553
1562
  )
1554
1563
 
1555
- # Extract question text (### heading)
1556
- question_text_match = re.search(
1557
- r"^###\s*\*\*(.+)\*\*", markdown_content, re.MULTILINE
1558
- )
1559
- question_text = (
1560
- question_text_match.group(1).strip()
1561
- if question_text_match
1562
- else None
1563
- )
1564
+ # # Extract question text (### heading)
1565
+ # question_text_match = re.search(
1566
+ # r"^###\s*\*\*(.+)\*\*", markdown_content, re.MULTILINE
1567
+ # )
1568
+ # question_text = (
1569
+ # question_text_match.group(1).strip()
1570
+ # if question_text_match
1571
+ # else None
1572
+ # )
1573
+
1574
+ # Extract question text enable multiple lines
1575
+ question_text = extract_question(markdown_content)
1564
1576
 
1565
1577
  # Extract OPTIONS (lines after #### options)
1566
1578
  options_match = re.search(
@@ -0,0 +1,138 @@
1
+ import argparse
2
+ import os
3
+ import nbformat as nbf
4
+
5
+
6
+ class MarkdownToNotebook:
7
+ def __init__(self, markdown_file: str):
8
+ """
9
+ Initializes the MarkdownToNotebook converter with the Markdown file path.
10
+
11
+ Args:
12
+ markdown_file (str): Path to the Markdown file to convert.
13
+ """
14
+ self.markdown_file = markdown_file
15
+
16
+ def convert_and_save(self):
17
+ """
18
+ Converts the Markdown file into a Jupyter Notebook and saves it with the same name.
19
+ """
20
+ if not os.path.exists(self.markdown_file):
21
+ print(f"Error: File '{self.markdown_file}' does not exist.")
22
+ return
23
+
24
+ notebook_name = os.path.splitext(self.markdown_file)[0] + ".ipynb"
25
+
26
+ with open(self.markdown_file, "r") as f:
27
+ content = f.read()
28
+
29
+ # Split content into lines
30
+ lines = content.splitlines()
31
+ nb = nbf.v4.new_notebook()
32
+ cells = []
33
+ current_cell = ""
34
+ current_type = None
35
+
36
+ for line in lines:
37
+ if line.startswith("# %%"):
38
+ if current_cell: # Save the previous cell
39
+ if current_type == "markdown":
40
+ cells.append(nbf.v4.new_markdown_cell(current_cell.strip()))
41
+ elif current_type == "code":
42
+ cells.append(nbf.v4.new_code_cell(current_cell.strip()))
43
+ elif current_type == "raw":
44
+ cells.append(nbf.v4.new_raw_cell(current_cell.strip()))
45
+
46
+ # Start a new cell
47
+ current_cell = ""
48
+ if "[markdown]" in line:
49
+ current_type = "markdown"
50
+ elif (
51
+ "# BEGIN" in line
52
+ or "# END" in line
53
+ or "# ASSIGNMENT CONFIG" in line
54
+ ):
55
+ current_type = "raw"
56
+ else:
57
+ current_type = "code"
58
+ else:
59
+ # Append lines to the current cell
60
+ if current_type == "markdown" and line.startswith("# "):
61
+ current_cell += line[2:] + "\n"
62
+ else:
63
+ current_cell += line + "\n"
64
+
65
+ # Save the last cell
66
+ if current_cell:
67
+ if current_type == "markdown":
68
+ cells.append(nbf.v4.new_markdown_cell(current_cell.strip()))
69
+ elif current_type == "code":
70
+ cells.append(nbf.v4.new_code_cell(current_cell.strip()))
71
+ elif current_type == "raw":
72
+ cell = nbf.v4.new_raw_cell(current_cell.strip())
73
+ cell["metadata"]["languageId"] = "raw"
74
+ cell["metadata"]["cell_type"] = "raw"
75
+ cells.append(cell)
76
+
77
+ nb["cells"] = cells
78
+
79
+ # Write the notebook
80
+ with open(notebook_name, "w") as f:
81
+ nbf.write(nb, f)
82
+
83
+ self.modify_notebook(notebook_name)
84
+
85
+ print(f"Notebook saved as: {notebook_name}")
86
+
87
+ @staticmethod
88
+ def modify_notebook(notebook_path: str):
89
+ """
90
+ Modifies an existing Jupyter Notebook by converting cells containing specific markers
91
+ ("# BEGIN T", "# END", "# ASSIGNMENT CONFIG") into raw cells.
92
+
93
+ Args:
94
+ notebook_path (str): Path to the existing Jupyter Notebook to modify.
95
+ """
96
+ if not os.path.exists(notebook_path):
97
+ print(f"Error: Notebook '{notebook_path}' does not exist.")
98
+ return
99
+
100
+ with open(notebook_path, "r") as f:
101
+ nb = nbf.read(f, as_version=4)
102
+
103
+ for cell in nb["cells"]:
104
+ if cell["cell_type"] == "code" and cell["source"].startswith(
105
+ (
106
+ "# BEGIN TESTS",
107
+ "# END TESTS",
108
+ "# END SOLUTION",
109
+ "# BEGIN QUESTION",
110
+ "# ASSIGNMENT CONFIG",
111
+ "# END TF",
112
+ "# BEGIN TF",
113
+ )
114
+ ):
115
+ cell["cell_type"] = "raw"
116
+ if "metadata" not in cell:
117
+ cell["metadata"] = {}
118
+ cell["metadata"] = {"vscode": {"languageId": "raw"}}
119
+
120
+ with open(notebook_path, "w") as f:
121
+ nbf.write(nb, f)
122
+
123
+
124
+ def main():
125
+ parser = argparse.ArgumentParser(
126
+ description="Convert a Markdown file with Jupyter-style cells into a Jupyter Notebook."
127
+ )
128
+ parser.add_argument(
129
+ "markdown_file", type=str, help="Path to the Markdown file to convert."
130
+ )
131
+
132
+ args = parser.parse_args()
133
+ converter = MarkdownToNotebook(markdown_file=args.markdown_file)
134
+ converter.convert_and_save()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
pykubegrader/utils.py CHANGED
@@ -15,7 +15,9 @@ 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
- random.shuffle(options)
18
+
19
+ for inner_list in options:
20
+ random.shuffle(inner_list)
19
21
 
20
22
  return options
21
23
 
@@ -4,6 +4,7 @@ import panel as pn
4
4
 
5
5
  from ..utils import list_of_lists
6
6
  from ..widgets_base.select import SelectQuestion
7
+ from .question_processor import process_questions_and_codes
7
8
 
8
9
  #
9
10
  # Style function
@@ -15,14 +16,22 @@ def MCQ(
15
16
  options: list[str] | list[list[str]],
16
17
  initial_vals: list[str],
17
18
  ) -> Tuple[list[pn.pane.HTML], list[pn.widgets.RadioButtonGroup]]:
18
- desc_width = "350px"
19
19
 
20
- desc_widgets = [
21
- pn.pane.HTML(
22
- f"<div style='text-align: left; width: {desc_width};'><b>{desc}</b></div>"
20
+ # Process descriptions through `process_questions_and_codes`
21
+ processed_titles, code_blocks = process_questions_and_codes(descriptions)
22
+
23
+ # Create rows for each description and its code block
24
+ desc_widgets = []
25
+ for title, code_block in zip(processed_titles, code_blocks):
26
+ # Create an HTML pane for the title
27
+ title_pane = pn.pane.HTML(
28
+ f"<div style='text-align: left; width: 100%;'><b>{title}</b></div>"
23
29
  )
24
- for desc in descriptions
25
- ]
30
+ # Add the title and code block in a row
31
+ if code_block:
32
+ desc_widgets.append(pn.Column(title_pane, code_block, sizing_mode="stretch_width"))
33
+ else:
34
+ desc_widgets.append(pn.Column(title_pane, sizing_mode="stretch_width"))
26
35
 
27
36
  radio_buttons = [
28
37
  pn.widgets.RadioBoxGroup(
@@ -0,0 +1,35 @@
1
+ import re
2
+ import panel as pn
3
+
4
+
5
+ def process_questions_and_codes(titles):
6
+ # Ensure titles is a list
7
+ if isinstance(titles, str):
8
+ titles = [titles]
9
+
10
+ processed_titles = []
11
+ code_blocks = []
12
+
13
+ for title in titles:
14
+ # Split the title at the "```python" delimiter
15
+ parts = title.split("```python", maxsplit=1)
16
+
17
+ # First part is the title, stripped of leading/trailing whitespace
18
+ title_without_code = parts[0].strip()
19
+
20
+ # remove aberrant ** from title
21
+ title_without_code = title_without_code.replace("**", "")
22
+
23
+ # Second part (if exists) contains the code block; split at closing ```
24
+ code = parts[1].split("```", maxsplit=1)[0].strip() if len(parts) > 1 else ""
25
+
26
+ # Append processed title
27
+ processed_titles.append(title_without_code)
28
+
29
+ # Append code block as Markdown if it exists
30
+ if code:
31
+ code_blocks.append(pn.pane.Markdown(f"```python\n{code}\n```"))
32
+ else:
33
+ code_blocks.append(None)
34
+
35
+ return processed_titles, code_blocks
@@ -91,4 +91,5 @@ class TFQuestion(SelectQuestion):
91
91
  options=[["True", "False"] for _ in range(len(keys))],
92
92
  descriptions=descriptions,
93
93
  points=points,
94
+ shuffle_answers=False,
94
95
  )
@@ -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, shuffle_options
8
8
  from ..widgets.style import drexel_colors, raw_css
9
9
 
10
10
  # Pass the custom CSS to Panel
@@ -56,6 +56,9 @@ class MultiSelectQuestion:
56
56
 
57
57
  self.initial_vals = [getattr(self, key) for key in self.keys]
58
58
 
59
+ # add shuffle options to multi_select.py
60
+ options = shuffle_options(options, seed)
61
+
59
62
  description_widgets, self.widgets = style(
60
63
  descriptions, options, self.initial_vals
61
64
  )
@@ -69,12 +72,14 @@ class MultiSelectQuestion:
69
72
  question_header = pn.pane.HTML(
70
73
  f"<h2>Question {self.question_number}: {title}</h2>"
71
74
  )
75
+
72
76
  question_body = pn.Column(
73
77
  *[
74
78
  pn.Row(desc_widget, checkbox_set)
75
79
  for desc_widget, checkbox_set in widget_pairs
76
80
  ]
77
81
  )
82
+
78
83
 
79
84
  self.layout = pn.Column(question_header, question_body, self.submit_button)
80
85
 
@@ -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, shuffle_options
8
8
  from ..widgets.style import drexel_colors
9
9
 
10
10
  # Pass custom CSS to Panel
@@ -24,6 +24,7 @@ class SelectQuestion:
24
24
  options: list,
25
25
  descriptions: list[str],
26
26
  points: int,
27
+ shuffle_answers: bool = True,
27
28
  ):
28
29
  responses = ensure_responses()
29
30
 
@@ -45,6 +46,9 @@ class SelectQuestion:
45
46
 
46
47
  self.initial_vals: list = [getattr(self, key) for key in self.keys]
47
48
 
49
+ if shuffle_answers:
50
+ options = shuffle_options(options, seed)
51
+
48
52
  desc_widgets, self.widgets = style(descriptions, options, self.initial_vals)
49
53
 
50
54
  self.submit_button = pn.widgets.Button(name="Submit", button_type="primary")