PyKubeGrader 0.0.4__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.
@@ -0,0 +1,84 @@
1
+ from .reading_base import ReadingPython
2
+
3
+
4
+ class ReadingPythonQuestion(ReadingPython):
5
+ def __init__(
6
+ self,
7
+ title="Reading, Commenting, and Interpreting Python Code",
8
+ question_number=4,
9
+ key="READING1",
10
+ options={
11
+ # General list of 15 potential comments with only one correct per line
12
+ "comments_options": [
13
+ None,
14
+ "Initializes a list `numbers` with integers and floats",
15
+ "Initializes the variable `total` with a value of 0",
16
+ "A `for` loop that iterates through an iterator `numbers`",
17
+ "`while` loop that continues to iterate until `total` is less than 9",
18
+ "Adds and assigns the variable `total` with the value of `num` plus 1",
19
+ "Statement that ends the `for` loop",
20
+ "Initializes a dictionary `numbers` with integers and floats",
21
+ "Initializes the class `total` with a value of 0",
22
+ "A `for` loop that iterates through an iterator `num`",
23
+ "`while` loop that continues to iterate until `total` is less than or equal to 9",
24
+ "Adds the variable `total` with the value of `num` plus 1",
25
+ "Statement that ends the `while` loop",
26
+ "`while` loop that continues to iterate while `total` is less than 9",
27
+ "Initializes a list `numbers` with integers and floats",
28
+ ],
29
+ "n_rows": 12, # Number of lines to show
30
+ "n_required": 8, # Number of rows required to respond
31
+ # Lines of code that require commenting
32
+ "lines_to_comment": [1, 2, 4, 5, 6, 7],
33
+ # Table headers
34
+ "table_headers": [
35
+ "Step",
36
+ "Line Number",
37
+ "Variable Changed",
38
+ "Current Value",
39
+ "DataType",
40
+ ],
41
+ # Variables Changed
42
+ "variables_changed": ["", "None", "numbers", "num", "total", "if", "else"],
43
+ "current_values": [
44
+ "",
45
+ "None",
46
+ "[5, 4.0]",
47
+ "[5.0, 4.0]",
48
+ "5.0",
49
+ "6.0",
50
+ "5",
51
+ "6",
52
+ "4.0",
53
+ "4",
54
+ "11.0",
55
+ "11",
56
+ "12",
57
+ "12.0",
58
+ "0",
59
+ "0.0",
60
+ "True",
61
+ "False",
62
+ "N/A",
63
+ ],
64
+ "datatypes": [
65
+ "",
66
+ "NoneType",
67
+ "list",
68
+ "dictionary",
69
+ "tuple",
70
+ "set",
71
+ "string",
72
+ "float",
73
+ "integer",
74
+ "boolean",
75
+ "N/A",
76
+ ],
77
+ },
78
+ points=[20, 25],
79
+ ):
80
+ super().__init__(
81
+ title=title,
82
+ question_number=question_number,
83
+ options=options,
84
+ )
@@ -0,0 +1,69 @@
1
+ from typing import Callable, Tuple
2
+
3
+ import panel as pn
4
+
5
+ from .misc import shuffle_questions
6
+ from .telemetry import ensure_responses, update_responses
7
+
8
+
9
+ class SelectQuestion:
10
+ def __init__(
11
+ self,
12
+ title: str,
13
+ style: Callable[
14
+ [list[str], list, list[str]],
15
+ Tuple[list[pn.pane.HTML], list[pn.widgets.Select]],
16
+ ],
17
+ question_number: int,
18
+ keys: list[str],
19
+ options: list,
20
+ descriptions: list[str],
21
+ points: int,
22
+ ):
23
+ responses = ensure_responses()
24
+
25
+ self.points = points
26
+ self.question_number = question_number
27
+ self.keys = keys
28
+ self.style = style
29
+
30
+ try:
31
+ seed: int = responses["seed"]
32
+ except ValueError:
33
+ raise ValueError(
34
+ "You must submit your student info before starting the exam"
35
+ )
36
+
37
+ # Dynamically assigning attributes based on keys, with default values from responses
38
+ for key in self.keys:
39
+ setattr(self, key, responses.get(key, None))
40
+
41
+ self.initial_vals: list = [getattr(self, key) for key in self.keys]
42
+
43
+ desc_widgets, self.widgets = style(descriptions, options, self.initial_vals)
44
+
45
+ self.submit_button = pn.widgets.Button(name="Submit", button_type="primary")
46
+ self.submit_button.on_click(self.submit)
47
+
48
+ widget_pairs = shuffle_questions(desc_widgets, self.widgets, seed)
49
+
50
+ self.layout = pn.Column(
51
+ f"# Question {self.question_number}: {title}",
52
+ *(pn.Row(desc_widget, dropdown) for desc_widget, dropdown in widget_pairs),
53
+ self.submit_button,
54
+ )
55
+
56
+ def submit(self, _) -> None:
57
+ selections = {key: widget.value for key, widget in zip(self.keys, self.widgets)}
58
+
59
+ for value in selections.values():
60
+ if value is None:
61
+ raise ValueError("Please answer all questions before submitting")
62
+
63
+ for key, value in selections.items():
64
+ update_responses(key, value)
65
+
66
+ print("Responses recorded successfully")
67
+
68
+ def show(self):
69
+ return self.layout
@@ -0,0 +1,101 @@
1
+ import panel as pn
2
+
3
+ from .multi_select_base import MultiSelectQuestion
4
+
5
+
6
+ def MultiSelect(
7
+ descriptions: list[str], options: list[list[str]], initial_vals: list[bool]
8
+ ):
9
+ desc_widgets: list[pn.pane.HTML] = []
10
+ checkboxes: list[pn.Column] = []
11
+
12
+ # Create separator line between questions
13
+ separator = pn.pane.HTML("<hr style='border:1px solid lightgray; width:100%;'>")
14
+
15
+ i = 0
16
+
17
+ for question, option_set in zip(descriptions, options):
18
+ desc_width = "500px"
19
+
20
+ # Create description widget with separator
21
+ desc_widget = pn.pane.HTML(
22
+ f"<hr style='border:1px solid lightgray; width:100%;'>"
23
+ f"<div style='text-align: left; width: {desc_width};'><b>{question}</b></div>"
24
+ )
25
+
26
+ # Create checkboxes for current question
27
+ checkbox_set = [
28
+ pn.widgets.Checkbox(
29
+ value=initial_vals[i],
30
+ name=option,
31
+ disabled=False,
32
+ )
33
+ for i, option in enumerate(option_set, start=i)
34
+ ]
35
+
36
+ desc_widgets.append(desc_widget)
37
+ checkboxes.append(pn.Column(*[separator] + checkbox_set))
38
+
39
+ # Increment iterator for next question
40
+ i += len(option_set)
41
+
42
+ return desc_widgets, checkboxes
43
+
44
+
45
+ class SelectMany(MultiSelectQuestion):
46
+ def __init__(
47
+ self,
48
+ title="Select all statements which are TRUE",
49
+ style=MultiSelect,
50
+ question_number=3,
51
+ keys=["MS1", "MS2", "MS3", "MS4", "MS5"],
52
+ options=[
53
+ ["`if` statements", "`for` loops", "`while` loops", "`end` statements"],
54
+ ["dictionary", "tuple", "float", "class"],
55
+ [
56
+ "A class can inherit attributes and methods from another class.",
57
+ "The `self` keyword is used to access variables that belong to a class.",
58
+ "`__init__` runs on instantiation of a class.",
59
+ "Variables assigned in a class are always globally accessible.",
60
+ ],
61
+ [
62
+ "Keys in dictionaries are mutable.",
63
+ "It is possible to store a list of dictionaries in Python.",
64
+ "You can create multiple instances of a class with different values.",
65
+ "If `list1` is a list and you assign it to `list2` and append a value to `list2`, `list1` will also contain the value that was appended to `list2`.",
66
+ ],
67
+ [
68
+ "In `print(i)`, `i` must be a string.",
69
+ "`2day` is not a valid variable name.",
70
+ "`i` is not defined when evaluating the `while` loop.",
71
+ "`i < 5` is not valid syntax to compare a variable `i` to an integer `5`, if `i` is a float.",
72
+ ],
73
+ ],
74
+ descriptions=[
75
+ "Which of the following control structures are used in Python? (Select all that apply)",
76
+ "Which of the following are built-in data structures in Python? (Select all that apply)",
77
+ "Concerning object-oriented programming in Python, which of the following statements are true? (Select all that apply)",
78
+ "Select all of the TRUE statements",
79
+ """
80
+ Select all the syntax errors in the following code:
81
+ <pre>
82
+ <code class="language-python">
83
+ 2day = 'Tuesday'
84
+ while i < 5:
85
+ print(i)
86
+ i += 1.0
87
+ </code>
88
+ </pre>
89
+ """,
90
+ ],
91
+ points=1,
92
+ ):
93
+ super().__init__(
94
+ title=title,
95
+ style=style,
96
+ question_number=question_number,
97
+ keys=keys,
98
+ options=options,
99
+ descriptions=descriptions,
100
+ points=points,
101
+ )
@@ -0,0 +1,132 @@
1
+ import base64
2
+ import datetime
3
+ import json
4
+ import logging
5
+ import os
6
+
7
+ import nacl.public
8
+ import requests
9
+ from IPython.core.interactiveshell import ExecutionInfo
10
+ from requests import Response
11
+ from requests.auth import HTTPBasicAuth
12
+
13
+ logging.basicConfig(filename=".output.log", level=logging.INFO, force=True)
14
+
15
+
16
+ def telemetry(info: ExecutionInfo) -> None:
17
+ cell_content = info.raw_cell
18
+ log_encrypted(f"code run: {cell_content}")
19
+
20
+
21
+ def encrypt_to_b64(message: str) -> str:
22
+ with open("server_public_key.bin", "rb") as f:
23
+ server_pub_key_bytes = f.read()
24
+ server_pub_key = nacl.public.PublicKey(server_pub_key_bytes)
25
+
26
+ with open("client_private_key.bin", "rb") as f:
27
+ client_private_key_bytes = f.read()
28
+ client_priv_key = nacl.public.PrivateKey(client_private_key_bytes)
29
+
30
+ box = nacl.public.Box(client_priv_key, server_pub_key)
31
+ encrypted = box.encrypt(message.encode())
32
+ encrypted_b64 = base64.b64encode(encrypted).decode("utf-8")
33
+
34
+ return encrypted_b64
35
+
36
+
37
+ def log_encrypted(message: str) -> None:
38
+ encrypted_b64 = encrypt_to_b64(message)
39
+ logging.info(f"Encrypted Output: {encrypted_b64}")
40
+
41
+
42
+ def log_variable(value, info_type) -> None:
43
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
44
+ message = f"{info_type}, {value}, {timestamp}"
45
+ log_encrypted(message)
46
+
47
+
48
+ def ensure_responses() -> dict:
49
+ with open(".responses.json", "a") as _:
50
+ pass
51
+
52
+ data = {}
53
+
54
+ try:
55
+ with open(".responses.json", "r") as f:
56
+ data = json.load(f)
57
+ except json.JSONDecodeError:
58
+ with open(".responses.json", "w") as f:
59
+ json.dump(data, f)
60
+
61
+ return data
62
+
63
+
64
+ def update_responses(key: str, value) -> dict:
65
+ data = ensure_responses()
66
+ data[key] = value
67
+
68
+ temp_path = ".responses.tmp"
69
+ orig_path = ".responses.json"
70
+
71
+ try:
72
+ with open(temp_path, "w") as f:
73
+ json.dump(data, f)
74
+
75
+ os.replace(temp_path, orig_path)
76
+ except (TypeError, json.JSONDecodeError) as e:
77
+ print(f"Failed to update responses: {e}")
78
+
79
+ if os.path.exists(temp_path):
80
+ os.remove(temp_path)
81
+
82
+ raise
83
+
84
+ return data
85
+
86
+
87
+ def score_question(
88
+ student_email: str,
89
+ term: str,
90
+ assignment: str,
91
+ question: str,
92
+ submission: str,
93
+ base_url: str = "http://localhost:8000",
94
+ ) -> Response:
95
+ url = base_url + "/live-scorer"
96
+
97
+ payload = {
98
+ "student_email": student_email,
99
+ "term": term,
100
+ "assignment": assignment,
101
+ "question": question,
102
+ "responses": submission,
103
+ }
104
+
105
+ res = requests.post(url, json=payload, auth=HTTPBasicAuth("student", "capture"))
106
+
107
+ return res
108
+
109
+
110
+ def submit_question_new(
111
+ student_email: str,
112
+ term: str,
113
+ assignment: str,
114
+ question: str,
115
+ responses: dict,
116
+ score: dict,
117
+ base_url: str = "http://localhost:8000",
118
+ ):
119
+ url = base_url + "/submit-question"
120
+
121
+ payload = {
122
+ "student_email": student_email,
123
+ "term": term,
124
+ "assignment": assignment,
125
+ "question": question,
126
+ "responses": responses,
127
+ "score": score,
128
+ }
129
+
130
+ res = requests.post(url, json=payload, auth=HTTPBasicAuth("student", "capture"))
131
+
132
+ return res
@@ -0,0 +1,77 @@
1
+ from typing import Tuple
2
+
3
+ import panel as pn
4
+
5
+ from .misc import list_of_lists
6
+ from .select_base import SelectQuestion
7
+
8
+
9
+ def MultipleChoice(
10
+ descriptions: list[str],
11
+ options: list[str] | list[list[str]],
12
+ initial_vals: list[str],
13
+ ) -> Tuple[list[pn.pane.HTML], list[pn.widgets.Select]]:
14
+ desc_width = "350px"
15
+
16
+ desc_widgets = [
17
+ pn.pane.HTML(
18
+ f"<div style='text-align: left; width: {desc_width};'><b>{desc}</b></div>"
19
+ )
20
+ for desc in descriptions
21
+ ]
22
+
23
+ dropdowns = [
24
+ pn.widgets.Select(options=option, value=value, width=300)
25
+ for value, option in zip(
26
+ initial_vals,
27
+ options if list_of_lists(options) else [options] * len(initial_vals),
28
+ )
29
+ ]
30
+
31
+ return desc_widgets, dropdowns
32
+
33
+
34
+ class TypesQuestion(SelectQuestion):
35
+ def __init__(
36
+ self,
37
+ title="Select the option that matches the definition:",
38
+ style=MultipleChoice,
39
+ question_number=1,
40
+ keys=["types1", "types2", "types3", "types4", "types5", "types6"],
41
+ options=[
42
+ "None",
43
+ "list",
44
+ "function",
45
+ "dictionary",
46
+ "array",
47
+ "variable",
48
+ "integer",
49
+ "string",
50
+ "tuple",
51
+ "iterator",
52
+ "float",
53
+ "object",
54
+ "class",
55
+ "module",
56
+ "package",
57
+ "instance",
58
+ ],
59
+ descriptions=[
60
+ "An ordered, mutable collection of items, defined with [ ]",
61
+ "A file containing Python definitions and statements",
62
+ "A collection of elements of the same type, allowing for efficient storage and manipulation of sequences of data",
63
+ "An immutable and ordered collection of elements in Python, which can contain mixed data types",
64
+ "A sequence of Unicode characters",
65
+ "A data type that represents real numbers with a decimal point",
66
+ ],
67
+ points=3,
68
+ ):
69
+ super().__init__(
70
+ title=title,
71
+ style=style,
72
+ question_number=question_number,
73
+ keys=keys,
74
+ options=options,
75
+ descriptions=descriptions,
76
+ points=points,
77
+ )