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,311 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ from datetime import datetime
7
+
8
+ import nacl.public
9
+ import numpy as np
10
+ import requests
11
+ from requests.auth import HTTPBasicAuth
12
+
13
+
14
+ def validate_logfile(
15
+ filepath: str,
16
+ assignment_id: str,
17
+ question_max_scores: dict[int, int],
18
+ free_response_questions=0,
19
+ username="student",
20
+ password="capture",
21
+ post_url="http://localhost:8000/upload-score",
22
+ login_url="http://localhost:8000/login",
23
+ ) -> None:
24
+ login_data = {
25
+ "username": username,
26
+ "password": password,
27
+ }
28
+
29
+ with open("server_private_key.bin", "rb") as priv_file:
30
+ server_private_key_bytes = priv_file.read()
31
+ server_priv_key = nacl.public.PrivateKey(server_private_key_bytes)
32
+
33
+ with open("client_public_key.bin", "rb") as pub_file:
34
+ client_public_key_bytes = pub_file.read()
35
+ client_pub_key = nacl.public.PublicKey(client_public_key_bytes)
36
+
37
+ box = nacl.public.Box(server_priv_key, client_pub_key)
38
+
39
+ with open(filepath, "r") as logfile:
40
+ encrypted_lines = logfile.readlines()
41
+
42
+ data_: list[str] = []
43
+ for line in encrypted_lines:
44
+ if "Encrypted Output: " in line:
45
+ trimmed = line.split("Encrypted Output: ")[1].strip()
46
+ decoded = base64.b64decode(trimmed)
47
+ decrypted = box.decrypt(decoded).decode()
48
+ data_.append(decrypted)
49
+
50
+ # Decoding the log file
51
+ # data_: list[str] = drexel_jupyter_logger.decode_log_file(self.filepath, key=key)
52
+ _loginfo = str(data_)
53
+
54
+ # Where possible, we should work with this reduced list of relevant entries
55
+ data_reduced = [
56
+ entry
57
+ for entry in data_
58
+ if re.match(r"info,", entry) or re.match(r"q\d+_\d+,", entry)
59
+ ]
60
+
61
+ # For debugging; to be commented out
62
+ with open(".output_reduced.log", "w") as f:
63
+ f.writelines(f"{item}\n" for item in data_reduced)
64
+
65
+ # Initialize the question scores and max scores
66
+ question_max_scores = question_max_scores
67
+ question_scores = {key: 0 for key in question_max_scores}
68
+
69
+ # Parsing the data to find the last entries for required fields
70
+ # This gets the student name etc.
71
+ last_entries: dict[str, str | float] = {}
72
+ for entry in data_reduced:
73
+ parts = [part.strip() for part in entry.split(",")]
74
+ if parts[0] == "info" and len(parts) == 4:
75
+ field_name = parts[1]
76
+ field_value = parts[2]
77
+ last_entries[field_name] = field_value
78
+
79
+ # For debugging; to be commented out
80
+ # print(f"Keys in last_entries dict: {last_entries.keys()}")
81
+
82
+ # Check if the assignment id is in the log file
83
+ if "assignment" not in last_entries or assignment_id != last_entries["assignment"]:
84
+ sys.exit(
85
+ "Your log file is not for the correct assignment. Please submit the correct log file."
86
+ )
87
+
88
+ required_student_info = ["drexel_id", "first_name", "last_name", "drexel_email"]
89
+
90
+ for field in required_student_info:
91
+ if last_entries.get(field) is None:
92
+ sys.exit(
93
+ "You must submit your student information before you start the exam. Please submit your information and try again."
94
+ )
95
+
96
+ # Initialize code and data lists
97
+ code: list[str] = []
98
+ data: list[str] = []
99
+
100
+ # Splitting the data into code and responses
101
+ for entry in data_:
102
+ # Splitting the data into code and responses
103
+ if "code run:" in entry:
104
+ code.append(entry)
105
+ else:
106
+ data.append(entry)
107
+
108
+ # Checks to see if the drexel_jupyter_logger is in the code
109
+ # If it is, the student might have tried to look at the solutions
110
+ # Commenting this out, since we're switching to asymmetric encryption
111
+ # flag = any("drexel_jupyter_logger" in item for item in code)
112
+
113
+ # Extracting timestamps and converting them to datetime objects
114
+ timestamps = [
115
+ datetime.strptime(row.split(",")[-1].strip(), "%Y-%m-%d %H:%M:%S")
116
+ for row in data_reduced
117
+ ]
118
+
119
+ # Getting the earliest and latest times
120
+ last_entries["start_time"] = min(timestamps).strftime("%Y-%m-%d %H:%M:%S")
121
+ last_entries["end_time"] = max(timestamps).strftime("%Y-%m-%d %H:%M:%S")
122
+ delta = max(timestamps) - min(timestamps)
123
+ minutes_rounded = round(delta.total_seconds() / 60, 2)
124
+ last_entries["elapsed_minutes"] = minutes_rounded
125
+ # last_entries["flag"] = flag
126
+
127
+ # Collect student info dict
128
+ student_information = {key.upper(): value for key, value in last_entries.items()}
129
+
130
+ # Write info dict to info.json
131
+ with open("info.json", "w") as file:
132
+ print("Writing to info.json")
133
+ json.dump(student_information, file)
134
+
135
+ def get_last_entry(data: list[str], field_name: str) -> str:
136
+ for entry in data[::-1]:
137
+ parts = [part.strip() for part in entry.split(",")]
138
+ if parts[0] == field_name:
139
+ return entry
140
+ return ""
141
+
142
+ def get_len_of_entries(data, question_number) -> int:
143
+ """function to get the unique entries by length
144
+
145
+ Args:
146
+ data (list): list of all the data records
147
+ question_number (int): question number to evaluate
148
+
149
+ Returns:
150
+ int: length of the unique entries
151
+ """
152
+
153
+ # Set for unique qN_* values
154
+ unique_qN_values = set()
155
+
156
+ for entry in data:
157
+ if entry.startswith(f"q{question_number}_"):
158
+ # Split the string by commas and get the value part
159
+ parts = [part.strip() for part in entry.split(",")]
160
+ # The value is the third element after splitting (?)
161
+ value = parts[0].split("_")[1]
162
+ unique_qN_values.add(value)
163
+
164
+ return len(unique_qN_values) + 1
165
+
166
+ # Modified list comprehension to filter as per the criteria
167
+ free_response = [
168
+ entry
169
+ for entry in data_
170
+ if entry.startswith("q")
171
+ and entry.split("_")[0][1:].isdigit()
172
+ and int(entry.split("_")[0][1:]) > free_response_questions
173
+ ]
174
+
175
+ # Initialize a dictionary to hold question entries.
176
+ q_entries = []
177
+
178
+ # Iterate over the number of free response questions.
179
+ for i in range(1, free_response_questions + 1):
180
+ # Collect entries for each question in a list.
181
+ entries = [
182
+ entry
183
+ for j in range(1, get_len_of_entries(data, i))
184
+ if (entry := get_last_entry(data, f"q{i}_{j}")) != ""
185
+ ]
186
+
187
+ # Store the list of entries in the dictionary, keyed by question number.
188
+ q_entries += entries
189
+
190
+ q_entries += free_response
191
+
192
+ # Parse the data
193
+ parsed_data: list[list[str]] = [
194
+ [part.strip() for part in line.split(",")] for line in q_entries
195
+ ]
196
+
197
+ unique_question_IDs = set(row[0] for row in parsed_data)
198
+
199
+ # Initialize a dictionary to hold the maximum score for each unique value
200
+ max_scores = {unique_value: 0 for unique_value in unique_question_IDs}
201
+
202
+ # Loop through each row in the data
203
+ for score_entry in parsed_data:
204
+ unique_value = score_entry[0]
205
+ score = int(score_entry[1])
206
+ # possible_score = float(row[3])
207
+ # Update the score if it's higher than the current maximum
208
+ if score > max_scores[unique_value]:
209
+ max_scores[unique_value] = score
210
+
211
+ # Loop through the max_scores dictionary and sum scores for each question
212
+ for unique_value, score in max_scores.items():
213
+ # Extract question number (assuming it's the number immediately after 'q')
214
+ question_number = int(unique_value.split("_")[0][1:])
215
+ question_scores[question_number] += score
216
+
217
+ # Sorting the dictionary by keys
218
+ question_max_scores = {
219
+ key: int(np.round(question_max_scores[key]))
220
+ for key in sorted(question_max_scores)
221
+ }
222
+
223
+ # Sorting the dictionary by keys
224
+ question_scores = {
225
+ key: int(np.round(question_scores[key])) for key in sorted(question_scores)
226
+ }
227
+
228
+ # Creating the dictionary structure
229
+ result_structure: dict[str, list[dict]] = {
230
+ "tests": [],
231
+ }
232
+
233
+ # Adding entries for each question
234
+ for question_number in question_scores.keys():
235
+ question_entry = {
236
+ "name": f"Question {question_number}",
237
+ "score": question_scores[question_number],
238
+ "max_score": question_max_scores[question_number],
239
+ # "visibility": "visible",
240
+ # "output": "",
241
+ }
242
+ result_structure["tests"].append(question_entry)
243
+
244
+ # Write results dict to results.json
245
+ with open("results.json", "w") as file:
246
+ print("Writing to results.json")
247
+ json.dump(result_structure, file, indent=4)
248
+
249
+ login_(login_data, login_url)
250
+
251
+ # The file to be uploaded. Ensure the path is correct.
252
+ file_path = "results.json"
253
+
254
+ # Construct data payload as a dict
255
+ final_data = {
256
+ "assignment": assignment_id,
257
+ "student_email": last_entries.get("drexel_email"),
258
+ # "original_file_name": file_path,
259
+ "start_time": last_entries["start_time"],
260
+ "end_time": last_entries["end_time"],
261
+ # "flag": last_entries["flag"],
262
+ # "submission_mechanism": "jupyter_notebook",
263
+ # "log_file": loginfo,
264
+ "scores": result_structure["tests"],
265
+ }
266
+
267
+ # Files to be uploaded. The key should match the name expected by the server.
268
+ _files = {
269
+ "file": (file_path, open(file_path, "rb")),
270
+ }
271
+
272
+ # Make the POST request with data and files
273
+ response = requests.post(
274
+ url=post_url,
275
+ json=final_data,
276
+ # files=files,
277
+ auth=HTTPBasicAuth(login_data["username"], login_data["password"]),
278
+ )
279
+
280
+ # Print messages for the user
281
+ submission_message(response)
282
+
283
+
284
+ def login_(login_data, login_url):
285
+ login_response = requests.post(
286
+ login_url, auth=HTTPBasicAuth(login_data["username"], login_data["password"])
287
+ )
288
+
289
+ if login_response.status_code == 200:
290
+ print("Login successful")
291
+ else:
292
+ Exception("Login failed")
293
+
294
+
295
+ def submission_message(response) -> None:
296
+ if response.status_code == 200:
297
+ print("Data successfully uploaded to the server")
298
+ print(response.text)
299
+ else:
300
+ print(f"Failed to upload data. Status code: {response.status_code}")
301
+ print(response.text)
302
+ print(
303
+ "There is something wrong with your log file or your submission. Please contact an instructor for help."
304
+ )
305
+
306
+ if os.path.exists("results.json"):
307
+ # os.remove("results.json")
308
+ # Let's keep results.json for now, for debugging
309
+ pass
310
+ else:
311
+ print("results.json was not present")