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