PyKubeGrader 0.0.4__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- PyKubeGrader-0.0.4.dist-info/LICENSE.txt +28 -0
- PyKubeGrader-0.0.4.dist-info/METADATA +69 -0
- PyKubeGrader-0.0.4.dist-info/RECORD +17 -0
- PyKubeGrader-0.0.4.dist-info/WHEEL +5 -0
- PyKubeGrader-0.0.4.dist-info/top_level.txt +1 -0
- pykubegrader/__init__.py +16 -0
- pykubegrader/widgets/info_widget.py +108 -0
- pykubegrader/widgets/mc_widget.py +72 -0
- pykubegrader/widgets/misc.py +29 -0
- pykubegrader/widgets/multi_select_base.py +99 -0
- pykubegrader/widgets/reading_base.py +168 -0
- pykubegrader/widgets/reading_widget.py +84 -0
- pykubegrader/widgets/select_base.py +69 -0
- pykubegrader/widgets/select_many_widget.py +101 -0
- pykubegrader/widgets/telemetry.py +132 -0
- pykubegrader/widgets/types_widget.py +77 -0
- pykubegrader/widgets/validate.py +311 -0
@@ -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
|
+
)
|