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.
- 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
|
+
)
|