PyKubeGrader 0.1.22__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyKubeGrader
3
- Version: 0.1.22
3
+ Version: 0.2.0
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -24,6 +24,8 @@ Requires-Dist: requests
24
24
  Requires-Dist: ruff
25
25
  Requires-Dist: setuptools
26
26
  Requires-Dist: sphinx
27
+ Requires-Dist: types-python-dateutil
28
+ Requires-Dist: types-pyyaml
27
29
  Requires-Dist: types-requests
28
30
  Requires-Dist: types-setuptools
29
31
  Provides-Extra: testing
@@ -0,0 +1,32 @@
1
+ pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
+ pykubegrader/initialize.py,sha256=t3iSdeIcndfY8LoHBVUEZfTW6sUWHyeFLirKo4GwSQE,3328
3
+ pykubegrader/telemetry.py,sha256=jRInaDqIpdeT7F0rxLJgO38lA-SMtmLcYE8nEAGah1Q,4922
4
+ pykubegrader/utils.py,sha256=T3GYnLnTL9VXjTZNPr00sUgMgobQYsNTGwynMyXdvHk,696
5
+ pykubegrader/validate.py,sha256=2KLSB3wfFZbBh1NGgmrOV073paKAgrQz4AgA6LmCIj4,11076
6
+ pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
+ pykubegrader/build/api_notebook_builder.py,sha256=FEaTj1fEsPAes7KNrPuhClEvKzLuOL3wG7Hgr7FQc-o,20083
8
+ pykubegrader/build/build_folder.py,sha256=nR06JHvgMio2p-95GsdqCXlfzZ6xJgAJNFPSjmg7QhI,70722
9
+ pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
10
+ pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
+ pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
12
+ pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
+ pykubegrader/log_parser/parse.ipynb,sha256=kPTo9gqhDineT7yT56P9xrQP1OKTf6Ro-iR4xlJ-or8,10610
14
+ pykubegrader/log_parser/parse.py,sha256=uw8lxWVh0FTaWi-bVmpMBbqwTXIHlJtB9gPc1qKvm4I,7040
15
+ pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
16
+ pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
17
+ pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
18
+ pykubegrader/widgets/select_many.py,sha256=l7YQ8QT5k71j36KC1f5LmKIAX2bXpvMDGc6nqIJ1PeQ,4116
19
+ pykubegrader/widgets/student_info.py,sha256=xhQgKehk1r5e6N_hnjAIovLdPvQju6ZqQTOiPG0aevg,3568
20
+ pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs,1656
21
+ pykubegrader/widgets/true_false.py,sha256=D45bjRLaAcNzsSlWPgxwTXGVZPE7PER34S30V6PjEXU,2807
22
+ pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
23
+ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
+ pykubegrader/widgets_base/multi_select.py,sha256=Cl0IN21wXLZuFu-zC65aS9tD4jMfzCRJ2DPjHao5_Ak,4044
25
+ pykubegrader/widgets_base/reading.py,sha256=_vjUPynqmJe_R4vf-7hVhGnQR726S9GL6qT8bflBXBM,5383
26
+ pykubegrader/widgets_base/select.py,sha256=Fw3uFNOIWo1a3CvlzSx23bvi6bSmA3TqutuRbhD4Dp8,2525
27
+ PyKubeGrader-0.2.0.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
28
+ PyKubeGrader-0.2.0.dist-info/METADATA,sha256=XEGRhBLAhcEcQVOhEkNcQi9orqofhyuNCQBwkALUJZ0,2729
29
+ PyKubeGrader-0.2.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
30
+ PyKubeGrader-0.2.0.dist-info/entry_points.txt,sha256=UPMdTT46fQwTYJWtrUwIWIbXbwyOPfNQgBFRa0frWzw,138
31
+ PyKubeGrader-0.2.0.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
32
+ PyKubeGrader-0.2.0.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  otter-folder-builder = pykubegrader.build.build_folder:main
3
+ otter-folder-cleaner = pykubegrader.build.clean_folder:main
@@ -0,0 +1 @@
1
+
@@ -93,7 +93,7 @@ class FastAPINotebookBuilder:
93
93
  self.replace_cell_source(cell_index, updated_cell_source)
94
94
 
95
95
  def compute_max_points_free_response(self):
96
- for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
96
+ for cell_dict in self.assertion_tests_dict.values():
97
97
  # gets the question name from the first cell to not double count
98
98
  if cell_dict["is_first"]:
99
99
  # get the max points for the question
@@ -380,7 +380,7 @@ class FastAPINotebookBuilder:
380
380
  question_groups[question].append(key)
381
381
 
382
382
  # Add 'is_first' and 'is_last' flags to all cells
383
- for question, keys in question_groups.items():
383
+ for keys in question_groups.values():
384
384
  test_number = 1
385
385
  for i, key in enumerate(keys):
386
386
  cells_dict[key]["is_first"] = i == 0
@@ -8,6 +8,16 @@ import shutil
8
8
  import subprocess
9
9
  import sys
10
10
  from dataclasses import dataclass, field
11
+ from datetime import datetime
12
+
13
+ import requests
14
+ import yaml
15
+ from dateutil import parser # For robust datetime parsing
16
+
17
+ try:
18
+ from pykubegrader.build.passwords import password, user
19
+ except: # noqa: E722
20
+ print("Passwords not found, cannot access database")
11
21
 
12
22
  import nbformat
13
23
 
@@ -44,8 +54,24 @@ class NotebookProcessor:
44
54
  Raises:
45
55
  OSError: If the solutions folder cannot be created due to permissions or other filesystem issues.
46
56
  """
57
+ if self.check_if_file_in_folder("assignment_config.yaml"):
58
+ # Parse the YAML content
59
+ with open(f"{self.root_folder}/assignment_config.yaml", "r") as file:
60
+ data = yaml.safe_load(file)
61
+ # Extract assignment details
62
+ assignment = data.get("assignment", {})
63
+ week_num = assignment.get("week")
64
+ self.assignment_type = assignment.get("assignment_type")
65
+ else:
66
+ self.assignment_type = self.assignment_tag.split("-")[0].lower()
67
+ week_num = self.assignment_tag.split("-")[-1]
68
+
69
+ self.week = f"week_{week_num}"
70
+
47
71
  # Define the folder to store solutions and ensure it exists
48
72
  self.solutions_folder = os.path.join(self.root_folder, "_solutions")
73
+ self.assignment_total_points = 0
74
+
49
75
  os.makedirs(
50
76
  self.solutions_folder, exist_ok=True
51
77
  ) # Create the folder if it doesn't exist
@@ -127,6 +153,84 @@ class NotebookProcessor:
127
153
  self.total_point_log, json_file, indent=4
128
154
  ) # `indent=4` for pretty formatting
129
155
 
156
+ if self.check_if_file_in_folder("assignment_config.yaml"):
157
+ self.add_assignment()
158
+
159
+ def build_payload(self, yaml_content):
160
+ """
161
+ Reads YAML content for an assignment and returns Python variables.
162
+
163
+ Args:
164
+ yaml_content (str): The YAML file path to parse.
165
+
166
+ Returns:
167
+ dict: A dictionary containing the parsed assignment data.
168
+ """
169
+ # Parse the YAML content
170
+ with open(yaml_content, "r") as file:
171
+ data = yaml.safe_load(file)
172
+
173
+ # Extract assignment details
174
+ assignment = data.get("assignment", {})
175
+ week = assignment.get("week")
176
+ assignment_type = assignment.get("assignment_type")
177
+ due_date_str = assignment.get("due_date")
178
+
179
+ # Convert due_date to a datetime object if available
180
+ due_date = None
181
+ if due_date_str:
182
+ try:
183
+ due_date = parser.parse(due_date_str) # Automatically handles timezones
184
+ except ValueError as e:
185
+ print(f"Error parsing due_date: {e}")
186
+
187
+ title = f"Week {week} - {assignment_type}"
188
+
189
+ # Return the extracted details as a dictionary
190
+ return {
191
+ "title": title,
192
+ "description": str(week),
193
+ "due_date": due_date,
194
+ "max_score": int(self.assignment_total_points),
195
+ }
196
+
197
+ def add_assignment(self):
198
+ """
199
+ Sends a POST request to add an assignment.
200
+ """
201
+ # Define the URL
202
+ url = "https://engr-131-api.eastus.cloudapp.azure.com/assignments"
203
+
204
+ # Build the payload
205
+ payload = self.build_payload(f"{self.root_folder}/assignment_config.yaml")
206
+
207
+ # Define HTTP Basic Authentication
208
+ auth = (user(), password())
209
+
210
+ # Define headers
211
+ headers = {"Content-Type": "application/json"}
212
+
213
+ # Serialize the payload with the custom JSON encoder
214
+ serialized_payload = json.dumps(payload, default=self.json_serial)
215
+
216
+ # Send the POST request
217
+ response = requests.post(
218
+ url, data=serialized_payload, headers=headers, auth=auth
219
+ )
220
+
221
+ # Print the response
222
+ print(f"Status Code: {response.status_code}")
223
+ try:
224
+ print(f"Response: {response.json()}")
225
+ except ValueError:
226
+ print(f"Response: {response.text}")
227
+
228
+ def check_if_file_in_folder(self, file):
229
+ for root, _, files in os.walk(self.root_folder):
230
+ if file in files:
231
+ return True
232
+ return False
233
+
130
234
  def _print_and_log(self, message):
131
235
  """
132
236
  Logs a message and optionally prints it to the console.
@@ -215,29 +319,42 @@ class NotebookProcessor:
215
319
  else:
216
320
  self._print_and_log(f"Notebook already in destination: {new_notebook_path}")
217
321
 
218
- solution_path_1, question_path = self.multiple_choice_parser(
322
+ solution_path_1, question_path_1 = self.multiple_choice_parser(
219
323
  temp_notebook_path, new_notebook_path
220
324
  )
221
- solution_path_2, question_path = self.true_false_parser(
325
+ solution_path_2, question_path_2 = self.true_false_parser(
222
326
  temp_notebook_path, new_notebook_path
223
327
  )
224
- solution_path_3, question_path = self.select_many_parser(
328
+ solution_path_3, question_path_3 = self.select_many_parser(
225
329
  temp_notebook_path, new_notebook_path
226
330
  )
227
331
 
228
332
  if any([solution_path_1, solution_path_2, solution_path_3]) is not None:
229
333
  solution_path = solution_path_1 or solution_path_2 or solution_path_3
230
334
 
335
+ if any([question_path_1, question_path_2, question_path_3]) is not None:
336
+ question_path = question_path_1 or question_path_2 or question_path_3
337
+
231
338
  student_notebook, self.otter_total_points = self.free_response_parser(
232
339
  temp_notebook_path, notebook_subfolder, notebook_name
233
340
  )
234
341
 
235
342
  # If Otter does not run, move the student file to the main directory
236
343
  if student_notebook is None:
344
+ clean_notebook(temp_notebook_path)
237
345
  path_ = shutil.copy(temp_notebook_path, self.root_folder)
346
+ path_2 = shutil.move(
347
+ question_path,
348
+ os.path.join(
349
+ os.path.dirname(temp_notebook_path), os.path.basename(question_path)
350
+ ),
351
+ )
238
352
  self._print_and_log(
239
353
  f"Copied and cleaned student notebook: {path_} -> {self.root_folder}"
240
354
  )
355
+ self._print_and_log(
356
+ f"Copied Questions to: {path_2} -> {os.path.join(os.path.dirname(temp_notebook_path), os.path.basename(question_path))}"
357
+ )
241
358
 
242
359
  # Move the solution file to the autograder folder
243
360
  if solution_path is not None:
@@ -298,6 +415,8 @@ class NotebookProcessor:
298
415
  + self.otter_total_points
299
416
  )
300
417
 
418
+ self.assignment_total_points += total_points
419
+
301
420
  self.total_point_log.update({notebook_name: total_points})
302
421
 
303
422
  def free_response_parser(
@@ -354,9 +473,9 @@ class NotebookProcessor:
354
473
  notebook_subfolder, "dist", "student", f"{notebook_name}.ipynb"
355
474
  )
356
475
 
357
- NotebookProcessor.add_initialization_code(student_notebook)
358
-
359
- self.clean_notebook(student_notebook)
476
+ NotebookProcessor.add_initialization_code(
477
+ student_notebook, self.week, self.assignment_type
478
+ )
360
479
 
361
480
  NotebookProcessor.replace_temp_in_notebook(
362
481
  student_notebook, student_notebook
@@ -367,6 +486,9 @@ class NotebookProcessor:
367
486
  NotebookProcessor.replace_temp_in_notebook(
368
487
  autograder_notebook, autograder_notebook
369
488
  )
489
+
490
+ clean_notebook(student_notebook)
491
+
370
492
  shutil.copy(student_notebook, self.root_folder)
371
493
  self._print_and_log(
372
494
  f"Copied and cleaned student notebook: {student_notebook} -> {self.root_folder}"
@@ -378,9 +500,18 @@ class NotebookProcessor:
378
500
 
379
501
  return student_notebook, out.total_points
380
502
  else:
381
- NotebookProcessor.add_initialization_code(temp_notebook_path)
503
+ NotebookProcessor.add_initialization_code(
504
+ temp_notebook_path, self.week, self.assignment_type
505
+ )
382
506
  return None, 0
383
507
 
508
+ @staticmethod
509
+ def json_serial(obj):
510
+ """JSON serializer for objects not serializable by default."""
511
+ if isinstance(obj, datetime):
512
+ return obj.isoformat()
513
+ raise TypeError(f"Type {type(obj)} not serializable")
514
+
384
515
  @staticmethod
385
516
  def remove_assignment_config_cells(notebook_path):
386
517
  # Read the notebook
@@ -399,13 +530,13 @@ class NotebookProcessor:
399
530
  nbformat.write(notebook, f)
400
531
 
401
532
  @staticmethod
402
- def add_initialization_code(notebook_path):
533
+ def add_initialization_code(notebook_path, week, assignment_type):
403
534
  # finds the first code cell
404
535
  index, cell = find_first_code_cell(notebook_path)
405
536
  cell = cell["source"]
406
537
  import_text = "from pykubegrader.initialize import initialize_assignment\n"
538
+ cell += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}", "{week}", "{assignment_type}" )\n'
407
539
  cell = f"{import_text}\n" + cell
408
- cell += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}")\n'
409
540
  replace_cell_source(notebook_path, index, cell)
410
541
 
411
542
  def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
@@ -426,15 +557,11 @@ class NotebookProcessor:
426
557
 
427
558
  data = NotebookProcessor.merge_metadata(value, data)
428
559
 
429
- for data_ in data:
430
- # Generate the solution file
431
- self.mcq_total_points = self.generate_solution_MCQ(
432
- data, output_file=solution_path
433
- )
560
+ self.mcq_total_points = self.generate_solution_MCQ(
561
+ data, output_file=solution_path
562
+ )
434
563
 
435
- question_path = (
436
- f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
437
- )
564
+ question_path = f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
438
565
 
439
566
  generate_mcq_file(data, output_file=question_path)
440
567
 
@@ -854,13 +981,6 @@ class NotebookProcessor:
854
981
  os.rename(old_file_path, new_file_path)
855
982
  logging.info(f"Renamed: {old_file_path} -> {new_file_path}")
856
983
 
857
- @staticmethod
858
- def clean_notebook(notebook_path):
859
- """
860
- Cleans a Jupyter notebook to remove unwanted cells and set cell metadata.
861
- """
862
- clean_notebook(notebook_path)
863
-
864
984
 
865
985
  def extract_raw_cells(ipynb_file, heading="# BEGIN MULTIPLE CHOICE"):
866
986
  """
@@ -1572,7 +1692,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
1572
1692
 
1573
1693
  # Define header lines
1574
1694
  header_lines = [
1575
- "from pykubegrader.widgets.true_false import TFQuestion, TrueFalse_style\n",
1695
+ "from pykubegrader.widgets.true_false import TFQuestion, TFStyle\n",
1576
1696
  "import pykubegrader.initialize\n",
1577
1697
  "import panel as pn\n\n",
1578
1698
  "pn.extension()\n\n",
@@ -1592,7 +1712,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
1592
1712
  f.write(" def __init__(self):\n")
1593
1713
  f.write(" super().__init__(\n")
1594
1714
  f.write(f" title=f'{q_value['question_text']}',\n")
1595
- f.write(" style=TrueFalse_style,\n")
1715
+ f.write(" style=TFStyle,\n")
1596
1716
  f.write(
1597
1717
  f" question_number={q_value['question number']},\n"
1598
1718
  )
@@ -0,0 +1,45 @@
1
+ import argparse
2
+ import os
3
+ import shutil
4
+ import sys
5
+
6
+
7
+ class FolderCleaner:
8
+ def __init__(self, root_folder: str):
9
+ """
10
+ Initializes the FolderCleaner with the root folder to clean.
11
+
12
+ Args:
13
+ root_folder (str): Path to the root folder to start cleaning.
14
+ """
15
+ self.root_folder = root_folder
16
+
17
+ def delete_dist_folders(self):
18
+ """
19
+ Recursively deletes all folders named 'dist' starting from the root folder.
20
+ """
21
+ for dirpath, dirnames, filenames in os.walk(self.root_folder, topdown=False):
22
+ if "dist" in dirnames:
23
+ dist_path = os.path.join(dirpath, "dist")
24
+ try:
25
+ shutil.rmtree(dist_path)
26
+ print(f"Deleted: {dist_path}")
27
+ except Exception as e:
28
+ print(f"Failed to delete {dist_path}: {e}")
29
+
30
+
31
+ def main():
32
+ parser = argparse.ArgumentParser(
33
+ description="Recursively delete all folders named 'dist' starting from a specified root folder."
34
+ )
35
+ parser.add_argument(
36
+ "root_folder", type=str, help="Path to the root folder to process"
37
+ )
38
+
39
+ args = parser.parse_args()
40
+ cleaner = FolderCleaner(root_folder=args.root_folder)
41
+ cleaner.delete_dist_folders()
42
+
43
+
44
+ if __name__ == "__main__":
45
+ sys.exit(main())
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,45 @@
1
+ import datetime
2
+
3
+ import numpy as np
4
+
5
+
6
+ def calculate_late_submission(
7
+ due: str,
8
+ submitted: str,
9
+ Q0: int = 100,
10
+ Q_min: int = 40,
11
+ k: float = 6.88e-5,
12
+ ) -> float:
13
+ """
14
+ Calculate the percentage value based on an exponential decay model
15
+ with respect to a due date, using datetime string inputs.
16
+
17
+ Parameters:
18
+ - due_date_str (str): The due date as a string in the format "%Y-%m-%d %H:%M:%S".
19
+ - submission_date (str): The comparison date as a string in the format "%Y-%m-%d %H:%M:%S".
20
+ - Q0 (float): Initial value (default is 100).
21
+ - Q_min (float): Minimum value (default is 40).
22
+ - k (float): Decay constant per minute (default is 6.88e-5).
23
+
24
+ Returns:
25
+ - float: The percentage value after decay, bounded between Q_min and Q0.
26
+ """
27
+
28
+ # Convert datetime strings to UNIX timestamps
29
+ due_date = datetime.datetime.strptime(due, "%Y-%m-%d %H:%M:%S")
30
+ submitted_date = datetime.datetime.strptime(submitted, "%Y-%m-%d %H:%M:%S")
31
+
32
+ # Calculate time difference in seconds
33
+ time_difference = (submitted_date - due_date).total_seconds()
34
+
35
+ # Convert time difference from seconds to minutes
36
+ time_in_minutes = time_difference / 60.0
37
+
38
+ # Calculate the exponential decay
39
+ Q: float = Q0 * np.exp(-k * time_in_minutes)
40
+
41
+ # Apply floor and ceiling conditions
42
+ Q = np.maximum(Q, Q_min)
43
+ Q = np.minimum(Q, Q0)
44
+
45
+ return Q
@@ -0,0 +1 @@
1
+
@@ -6,7 +6,7 @@
6
6
  "metadata": {},
7
7
  "outputs": [],
8
8
  "source": [
9
- "from .parse import LogParser\n",
9
+ "from parse import LogParser\n",
10
10
  "\n",
11
11
  "# ----------------- Example usage -----------------\n",
12
12
  "\n",
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Dict, List, Optional
2
+ from typing import Optional
3
3
 
4
4
 
5
5
  @dataclass
@@ -9,16 +9,16 @@ class LogParser:
9
9
  Handles both assignment info and question-level details.
10
10
  """
11
11
 
12
- log_lines: List[str]
12
+ log_lines: list[str]
13
13
  week_tag: Optional[str] = None
14
- student_info: Dict[str, str] = field(default_factory=dict)
15
- assignments: Dict[str, Dict] = field(default_factory=dict)
14
+ student_info: dict[str, str] = field(default_factory=dict)
15
+ assignments: dict[str, dict] = field(default_factory=dict)
16
16
 
17
- def parse_logs(self):
17
+ def parse_logs(self) -> None:
18
18
  """
19
19
  Main method to parse logs and populate student_info and assignments.
20
20
  """
21
- unique_students = set()
21
+ unique_students: set[str] = set()
22
22
 
23
23
  self._find_all_questions()
24
24
 
@@ -41,13 +41,13 @@ class LogParser:
41
41
  ):
42
42
  self._process_assignment_entry(line)
43
43
 
44
- def _find_all_questions(self):
44
+ def _find_all_questions(self) -> None:
45
45
  """
46
46
  Finds all questions in the log_lines and returns a list of them.
47
47
  """
48
48
  questions = []
49
49
  for line in self.log_lines:
50
- if self.week_tag in line:
50
+ if self.week_tag and self.week_tag in line:
51
51
  parts = line.split(",")
52
52
  question_tag = parts[3].strip()
53
53
  if question_tag not in questions:
@@ -60,7 +60,7 @@ class LogParser:
60
60
  """
61
61
  return line.startswith("Student Info")
62
62
 
63
- def _process_student_info(self, line: str, unique_students: set):
63
+ def _process_student_info(self, line: str, unique_students: set) -> None:
64
64
  """
65
65
  Processes a line containing student information.
66
66
  Raises an error if multiple unique students are found.
@@ -83,7 +83,7 @@ class LogParser:
83
83
  "timestamp": parts[3].strip(),
84
84
  }
85
85
 
86
- def _process_assignment_header(self, line: str):
86
+ def _process_assignment_header(self, line: str) -> None:
87
87
  parts = line.split(",")
88
88
  assignment_tag = parts[0].strip()
89
89
  if assignment_tag.startswith("total-points"):
@@ -105,7 +105,7 @@ class LogParser:
105
105
  self.assignments[notebook_name]["max_points"] = total_points_value
106
106
  self.assignments[notebook_name]["latest_timestamp"] = timestamp
107
107
 
108
- def _process_assignment_entry(self, line: str):
108
+ def _process_assignment_entry(self, line: str) -> None:
109
109
  """
110
110
  Processes a line containing an assignment entry.
111
111
  Adds it to the assignments dictionary.
@@ -141,7 +141,7 @@ class LogParser:
141
141
  if timestamp > self.assignments[assignment_tag]["latest_timestamp"]:
142
142
  self.assignments[assignment_tag]["latest_timestamp"] = timestamp
143
143
 
144
- def _extract_total_points(self, parts: List[str]) -> Optional[float]:
144
+ def _extract_total_points(self, parts: list[str]) -> Optional[float]:
145
145
  """
146
146
  Extracts the total-points value from the parts array of a total-points line.
147
147
  """
@@ -150,17 +150,17 @@ class LogParser:
150
150
  except (ValueError, IndexError):
151
151
  return None
152
152
 
153
- def calculate_total_scores(self):
153
+ def calculate_total_scores(self) -> None:
154
154
  """
155
155
  Calculates total scores for each assignment by summing the 'score_earned'
156
156
  of its questions, and sets 'total_points' if it was not specified.
157
157
  """
158
- for assignment, data in self.assignments.items():
158
+ for data in self.assignments.values():
159
159
  # Sum of all question score_earned
160
160
  total_score = sum(q["score_earned"] for q in data["questions"].values())
161
161
  data["total_score"] = total_score
162
162
 
163
- def get_results(self) -> Dict[str, Dict]:
163
+ def get_results(self) -> dict[str, dict]:
164
164
  """
165
165
  Returns the parsed results as a hierarchical dictionary with three sections:
166
166
  """
pykubegrader/telemetry.py CHANGED
@@ -11,27 +11,24 @@ from IPython.core.interactiveshell import ExecutionInfo
11
11
  from requests import Response
12
12
  from requests.auth import HTTPBasicAuth
13
13
 
14
- # Logger for .output_code.log
14
+ #
15
+ # Logging setup
16
+ #
17
+
18
+ # Logger for cell execution
15
19
  logger_code = logging.getLogger("code_logger")
16
20
  logger_code.setLevel(logging.INFO)
17
21
 
18
22
  file_handler_code = logging.FileHandler(".output_code.log")
19
23
  file_handler_code.setLevel(logging.INFO)
20
-
21
- # formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
22
- # file_handler_code.setFormatter(formatter)
23
-
24
24
  logger_code.addHandler(file_handler_code)
25
25
 
26
- # Logger for .output_reduced.log
26
+ # Logger for question scores etc.
27
27
  logger_reduced = logging.getLogger("reduced_logger")
28
28
  logger_reduced.setLevel(logging.INFO)
29
29
 
30
30
  file_handler_reduced = logging.FileHandler(".output_reduced.log")
31
31
  file_handler_reduced.setLevel(logging.INFO)
32
-
33
- # file_handler_reduced.setFormatter(formatter)
34
-
35
32
  logger_reduced.addHandler(file_handler_reduced)
36
33
 
37
34
  #
@@ -55,7 +52,7 @@ def encrypt_to_b64(message: str) -> str:
55
52
  return encrypted_b64
56
53
 
57
54
 
58
- def ensure_responses() -> dict:
55
+ def ensure_responses() -> dict[str, Any]:
59
56
  with open(".responses.json", "a") as _:
60
57
  pass
61
58
 
@@ -125,28 +122,34 @@ def update_responses(key: str, value) -> dict:
125
122
  #
126
123
 
127
124
 
128
- # If we instead call this with **responses
125
+ # TODO: Improve error handling
129
126
  def score_question(
130
- student_email: str,
131
- assignment: str,
132
- question: str,
133
- submission: str,
134
127
  term: str = "winter_2025",
135
- base_url: str = "https://engr-131-api.eastus.cloudapp.azure.com/",
136
- ) -> Response:
128
+ base_url: str = "https://engr-131-api.eastus.cloudapp.azure.com",
129
+ ) -> None:
137
130
  url = base_url + "/live-scorer"
138
131
 
139
- payload = {
140
- "student_email": student_email,
132
+ responses = ensure_responses()
133
+
134
+ payload: dict[str, Any] = {
135
+ "student_email": f'{responses["jhub_user"]}@drexel.edu',
141
136
  "term": term,
142
- "assignment": assignment,
143
- "question": question,
144
- "responses": submission,
137
+ "week": responses["week"],
138
+ "assignment": responses["assignment_type"],
139
+ "question": f'_{responses["assignment"]}',
140
+ "responses": responses,
145
141
  }
146
142
 
147
143
  res = requests.post(url, json=payload, auth=HTTPBasicAuth("student", "capture"))
148
144
 
149
- return res
145
+ res_data: dict[str, tuple[float, float]] = res.json()
146
+
147
+ for question, (points_earned, max_points) in res_data.items():
148
+ log_variable(
149
+ assignment_name=responses["assignment"],
150
+ value=f"{points_earned}, {max_points}",
151
+ info_type=question,
152
+ )
150
153
 
151
154
 
152
155
  def submit_question(
@@ -157,7 +160,7 @@ def submit_question(
157
160
  responses: dict,
158
161
  score: dict,
159
162
  base_url: str = "https://engr-131-api.eastus.cloudapp.azure.com/",
160
- ):
163
+ ) -> Response:
161
164
  url = base_url + "/submit-question"
162
165
 
163
166
  payload = {
@@ -174,7 +177,7 @@ def submit_question(
174
177
  return res
175
178
 
176
179
 
177
- # TODO: refine function
180
+ # TODO: Refine
178
181
  def verify_server(
179
182
  jhub_user: Optional[str] = None,
180
183
  url: str = "https://engr-131-api.eastus.cloudapp.azure.com/",
@@ -183,43 +186,3 @@ def verify_server(
183
186
  res = requests.get(url, params=params)
184
187
  message = f"status code: {res.status_code}"
185
188
  return message
186
-
187
-
188
- # TODO: implement function; or maybe not?
189
- # At least improve other one
190
- def score_question_improved(
191
- week: str,
192
- assignment_category: str,
193
- term: str = "winter_2025",
194
- base_url: str = "https://engr-131-api.eastus.cloudapp.azure.com",
195
- ) -> None:
196
- url = base_url + "/live-scorer"
197
-
198
- responses = ensure_responses()
199
-
200
- payload: dict[str, Any] = {
201
- "student_email": f'{responses["jhub_user"]}@drexel.edu',
202
- "term": term,
203
- "week": week,
204
- "assignment": assignment_category,
205
- "question": f'_{responses["assignment"]}',
206
- "responses": responses,
207
- }
208
-
209
- res = requests.post(url, json=payload, auth=HTTPBasicAuth("student", "capture"))
210
-
211
- res_data = res.json()
212
- # max_points, points_earned = res_data["max_points"], res_data["points_earned"]
213
- # log_variable(
214
- # assignment_name=responses["assignment"],
215
- # value=f"{points_earned}, {max_points}",
216
- # info_type="score",
217
- # )
218
-
219
- # res_data is now dict[str, tuple[float, float]]
220
- for question, (points_earned, max_points) in res_data.items():
221
- log_variable(
222
- assignment_name=responses["assignment"],
223
- value=f"{points_earned}, {max_points}",
224
- info_type=question,
225
- )
pykubegrader/utils.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import random
2
- from typing import Tuple
2
+ from typing import Optional, Tuple
3
3
 
4
4
  import panel as pn
5
5
 
@@ -8,7 +8,7 @@ def list_of_lists(options: list) -> bool:
8
8
  return all(isinstance(elem, list) for elem in options)
9
9
 
10
10
 
11
- def shuffle_options(options, seed: int):
11
+ def shuffle_options(options: list[Optional[str]], seed: int) -> list[Optional[str]]:
12
12
  random.seed(seed)
13
13
  random.shuffle(options)
14
14
 
pykubegrader/validate.py CHANGED
@@ -19,18 +19,20 @@ def validate_logfile(
19
19
  filepath: str,
20
20
  assignment_id: str,
21
21
  question_max_scores: dict[int, int],
22
- free_response_questions=0,
23
- username="student",
24
- password="capture",
25
- base_url="https://engr-131-api.eastus.cloudapp.azure.com",
22
+ free_response_questions: int = 0,
23
+ username: str = "student",
24
+ password: str = "capture",
25
+ base_url: str = "https://engr-131-api.eastus.cloudapp.azure.com",
26
+ key_box=None,
26
27
  ) -> None:
27
28
  login_data = {
28
29
  "username": username,
29
30
  "password": password,
30
31
  }
31
32
 
32
- # Generate box from private and public keys
33
- key_box = generate_keys()
33
+ if key_box is None:
34
+ # Generate box from private and public keys
35
+ key_box = generate_keys()
34
36
 
35
37
  decrypted_log, log_reduced = read_logfile(filepath, key_box)
36
38
 
@@ -225,7 +227,7 @@ def validate_logfile(
225
227
  submission_message(response)
226
228
 
227
229
 
228
- def read_logfile(filepath, key_box=None) -> tuple[list[str], list[str]]:
230
+ def read_logfile(filepath: str, key_box=None) -> tuple[list[str], list[str]]:
229
231
  if key_box is None:
230
232
  key_box = generate_keys()
231
233
 
@@ -307,7 +309,7 @@ def get_last_entry(data: list[str], field_name: str) -> str:
307
309
  return ""
308
310
 
309
311
 
310
- def submission_message(response) -> None:
312
+ def submission_message(response: requests.Response) -> None:
311
313
  if response.status_code == 200:
312
314
  print("Data successfully uploaded to the server")
313
315
  print(response.text)
@@ -326,7 +328,7 @@ def submission_message(response) -> None:
326
328
  print("results.json was not present")
327
329
 
328
330
 
329
- def verify_login(login_data, login_url):
331
+ def verify_login(login_data: dict[str, str], login_url: str) -> None:
330
332
  login_response = requests.post(
331
333
  login_url, auth=HTTPBasicAuth(login_data["username"], login_data["password"])
332
334
  )
@@ -1,19 +1 @@
1
- # Auto-generated __init__.py
2
1
 
3
- from . import (
4
- multiple_choice,
5
- reading_question,
6
- select_many,
7
- student_info,
8
- true_false,
9
- types_question,
10
- )
11
-
12
- __all__ = [
13
- "select_many",
14
- "multiple_choice",
15
- "true_false",
16
- "reading_question",
17
- "student_info",
18
- "types_question",
19
- ]
@@ -13,7 +13,7 @@ pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
13
13
  #
14
14
 
15
15
 
16
- def TrueFalse_style(
16
+ def TFStyle(
17
17
  descriptions: List[str],
18
18
  options: List[str] | List[List[str]],
19
19
  initial_vals: List[str],
@@ -71,7 +71,7 @@ class TFQuestion(SelectQuestion):
71
71
  def __init__(
72
72
  self,
73
73
  title="Select if the statement is True or False",
74
- style=TrueFalse_style,
74
+ style=TFStyle,
75
75
  question_number=2,
76
76
  keys=["MC1", "MC2", "MC3", "MC4"],
77
77
  options=None,
@@ -3,7 +3,7 @@ from typing import Callable, Tuple
3
3
 
4
4
  import panel as pn
5
5
 
6
- from ..telemetry import ensure_responses, update_responses
6
+ from ..telemetry import ensure_responses, score_question, update_responses
7
7
  from ..utils import shuffle_questions
8
8
  from ..widgets.style import drexel_colors, raw_css
9
9
 
@@ -104,6 +104,8 @@ class MultiSelectQuestion:
104
104
 
105
105
  self.record_responses(responses_flat)
106
106
 
107
+ score_question() # Debugging; update later
108
+
107
109
  def record_responses(self, responses_flat: list[bool]) -> None:
108
110
  for key, value in zip(self.keys, responses_flat):
109
111
  update_responses(key, value)
@@ -3,7 +3,7 @@ from typing import Optional
3
3
 
4
4
  import panel as pn
5
5
 
6
- from ..telemetry import ensure_responses, update_responses
6
+ from ..telemetry import ensure_responses, score_question, update_responses
7
7
  from ..utils import shuffle_options
8
8
 
9
9
 
@@ -162,6 +162,8 @@ class ReadingPython:
162
162
  i += 1
163
163
  update_responses(f"q{self.question_number}_{i}", exec_val)
164
164
 
165
+ score_question() # Debugging; update later
166
+
165
167
  print("Responses recorded successfully")
166
168
 
167
169
  def show(self):
@@ -3,7 +3,7 @@ from typing import Callable, Tuple
3
3
 
4
4
  import panel as pn
5
5
 
6
- from ..telemetry import ensure_responses, update_responses
6
+ from ..telemetry import ensure_responses, score_question, update_responses
7
7
  from ..utils import shuffle_questions
8
8
  from ..widgets.style import drexel_colors
9
9
 
@@ -71,6 +71,8 @@ class SelectQuestion:
71
71
  for key, value in selections.items():
72
72
  update_responses(key, value)
73
73
 
74
+ score_question() # Debugging; update later
75
+
74
76
  # Temporarily change button text to indicate submission
75
77
  self.submit_button.name = "Responses Submitted"
76
78
  time.sleep(1)
@@ -1,27 +0,0 @@
1
- pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
- pykubegrader/initialize.py,sha256=t3iSdeIcndfY8LoHBVUEZfTW6sUWHyeFLirKo4GwSQE,3328
3
- pykubegrader/telemetry.py,sha256=IVQISKPfJX7IJ4tCG9AOO36UlTtbVZv2rZnrv3-ZSIc,5994
4
- pykubegrader/utils.py,sha256=dKw6SyRYU3DWRgD3xER7wq-C9e1daWPkqr901LpcwiQ,642
5
- pykubegrader/validate.py,sha256=vEdNN386yFloDRcjMDrTAqfBmeCXGcDPNH_rLZScIm8,10945
6
- pykubegrader/build/api_notebook_builder.py,sha256=GVi6hupfQaWeFMv6Bdela3FTRHvOQYXPIcICnkaLhgA,20119
7
- pykubegrader/build/build_folder.py,sha256=_mVEUiPwOPARBHkucAOw2bP8XzmspfL6B4-Ic_Ey3I0,66493
8
- pykubegrader/log_parser/parse.ipynb,sha256=F3ZWi5_AOxEnSihY0VBz4jjqo0__GggjRgFvS0QCHTg,10611
9
- pykubegrader/log_parser/parse.py,sha256=aON6tWj0dFJcYR9GmzXWfmZ4_t8LU1FTq6vbWCePgRs,6987
10
- pykubegrader/widgets/__init__.py,sha256=s3ky3eJDa1RedFVdpKxmqv6mHBYpOSL9Z6qThSH9cbs,303
11
- pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
12
- pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
13
- pykubegrader/widgets/select_many.py,sha256=l7YQ8QT5k71j36KC1f5LmKIAX2bXpvMDGc6nqIJ1PeQ,4116
14
- pykubegrader/widgets/student_info.py,sha256=xhQgKehk1r5e6N_hnjAIovLdPvQju6ZqQTOiPG0aevg,3568
15
- pykubegrader/widgets/style.py,sha256=fVBMYy_a6Yoz21avNpiORWC3f5FD-OrVpaZ3npmunvs,1656
16
- pykubegrader/widgets/true_false.py,sha256=pE2FjvX6WQ-Z423N40nTWMtudtGS7LiNXZ5dERk6uWs,2823
17
- pykubegrader/widgets/types_question.py,sha256=kZdRRXyFzOtYTmGdC7XWb_2oaxqg1WSuLcQn_sTj6Qc,2300
18
- pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
- pykubegrader/widgets_base/multi_select.py,sha256=jzuK_1eLyhDOzR1jragLRBf_aO8IEaoixfozxBeMBs0,3975
20
- pykubegrader/widgets_base/reading.py,sha256=4uTLmlPzCwxVzufFhPjM7W19uMGguRb6y4eAV3x-zAc,5314
21
- pykubegrader/widgets_base/select.py,sha256=b5mmd-Cl1A2T2ePZ20-KLVyvP3bzvzYX36n3lMcrcFM,2456
22
- PyKubeGrader-0.1.22.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
23
- PyKubeGrader-0.1.22.dist-info/METADATA,sha256=AeAp-atdN0IzDgWASkN7M-AtvFD4j2GvCE3Q_aGfCTQ,2665
24
- PyKubeGrader-0.1.22.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
25
- PyKubeGrader-0.1.22.dist-info/entry_points.txt,sha256=Kd4Bh-i3hc4qlnLU1p0nc8yPw9cC5AQGOtkk2eLGnQw,78
26
- PyKubeGrader-0.1.22.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
27
- PyKubeGrader-0.1.22.dist-info/RECORD,,