PyKubeGrader 0.1.13__tar.gz → 0.1.15__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. {pykubegrader-0.1.13/src/PyKubeGrader.egg-info → pykubegrader-0.1.15}/PKG-INFO +1 -1
  2. {pykubegrader-0.1.13 → pykubegrader-0.1.15/src/PyKubeGrader.egg-info}/PKG-INFO +1 -1
  3. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/SOURCES.txt +2 -0
  4. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/build/api_notebook_builder.py +31 -6
  5. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/build/build_folder.py +67 -10
  6. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/initialize.py +3 -1
  7. pykubegrader-0.1.15/src/pykubegrader/log_parser/parse.ipynb +214 -0
  8. pykubegrader-0.1.15/src/pykubegrader/log_parser/parse.py +184 -0
  9. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/telemetry.py +37 -7
  10. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/validate.py +31 -22
  11. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/.coveragerc +0 -0
  12. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/.github/workflows/main.yml +0 -0
  13. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/.gitignore +0 -0
  14. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/.readthedocs.yml +0 -0
  15. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/AUTHORS.rst +0 -0
  16. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/CHANGELOG.rst +0 -0
  17. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/CONTRIBUTING.rst +0 -0
  18. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/LICENSE.txt +0 -0
  19. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/README.rst +0 -0
  20. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/Makefile +0 -0
  21. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
  22. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
  23. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/_static/custom.css +0 -0
  24. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/authors.rst +0 -0
  25. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/changelog.rst +0 -0
  26. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/conf.py +0 -0
  27. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/contributing.rst +0 -0
  28. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/index.rst +0 -0
  29. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/license.rst +0 -0
  30. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/readme.rst +0 -0
  31. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/docs/requirements.txt +0 -0
  32. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/examples/.responses.json +0 -0
  33. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/examples/true_false.ipynb +0 -0
  34. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/pyproject.toml +0 -0
  35. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/setup.cfg +0 -0
  36. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/setup.py +0 -0
  37. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
  38. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
  39. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
  40. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/requires.txt +0 -0
  41. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
  42. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/__init__.py +0 -0
  43. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/utils.py +0 -0
  44. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/__init__.py +0 -0
  45. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/multiple_choice.py +0 -0
  46. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/reading_question.py +0 -0
  47. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/select_many.py +0 -0
  48. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/student_info.py +0 -0
  49. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/style.py +0 -0
  50. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/true_false.py +0 -0
  51. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets/types_question.py +0 -0
  52. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets_base/__init__.py +0 -0
  53. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets_base/multi_select.py +0 -0
  54. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets_base/reading.py +0 -0
  55. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/src/pykubegrader/widgets_base/select.py +0 -0
  56. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/tests/conftest.py +0 -0
  57. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/tests/import_test.py +0 -0
  58. {pykubegrader-0.1.13 → pykubegrader-0.1.15}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyKubeGrader
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyKubeGrader
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -39,6 +39,8 @@ src/pykubegrader/utils.py
39
39
  src/pykubegrader/validate.py
40
40
  src/pykubegrader/build/api_notebook_builder.py
41
41
  src/pykubegrader/build/build_folder.py
42
+ src/pykubegrader/log_parser/parse.ipynb
43
+ src/pykubegrader/log_parser/parse.py
42
44
  src/pykubegrader/widgets/__init__.py
43
45
  src/pykubegrader/widgets/multiple_choice.py
44
46
  src/pykubegrader/widgets/reading_question.py
@@ -13,11 +13,13 @@ import nbformat
13
13
  class FastAPINotebookBuilder:
14
14
  notebook_path: str
15
15
  temp_notebook: Optional[str] = None
16
+ assignment_tag: Optional[str] = ""
16
17
 
17
18
  def __post_init__(self):
18
19
  self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
19
20
  self.notebook_path
20
21
  )
22
+ self.total_points = 0
21
23
  self.run()
22
24
 
23
25
  def run(self):
@@ -34,6 +36,8 @@ class FastAPINotebookBuilder:
34
36
  self.add_api_code()
35
37
 
36
38
  def add_api_code(self):
39
+ self.compute_max_points_free_response()
40
+
37
41
  for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
38
42
  print(
39
43
  f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
@@ -75,21 +79,32 @@ class FastAPINotebookBuilder:
75
79
  ["earned_points = float(os.environ.get('EARNED_POINTS', 0))\n"]
76
80
  )
77
81
  updated_cell_source.extend(["earned_points += score\n"])
82
+
83
+ short_filename = self.filename.split(".")[0].replace("_temp", "")
78
84
  updated_cell_source.extend(
79
85
  [
80
- f'log_variable(f"{{score}}, {{max_score}}", question_id, "{self.filename.split(".")[0]}")\n'
86
+ f'log_variable("{short_filename}",f"{{score}}, {{max_score}}", question_id)\n'
81
87
  ]
82
88
  )
83
89
  updated_cell_source.extend(
84
90
  ["os.environ['EARNED_POINTS'] = str(earned_points)\n"]
85
91
  )
86
92
 
87
- # cell_source = FastAPINotebookBuilder.insert_list_at_index(
88
- # cell_source, updated_cell_source, last_import_line_ind + 1
89
- # )
90
-
91
93
  self.replace_cell_source(cell_index, updated_cell_source)
92
94
 
95
+ def compute_max_points_free_response(self):
96
+ for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
97
+ # gets the question name from the first cell to not double count
98
+ if cell_dict["is_first"]:
99
+ # get the max points for the question
100
+ max_question_points = sum(
101
+ cell["points"]
102
+ for cell in self.assertion_tests_dict.values()
103
+ if cell["question"] == cell_dict["question"]
104
+ )
105
+
106
+ self.total_points += max_question_points
107
+
93
108
  def construct_first_cell_question_header(self, cell_dict):
94
109
  max_question_points = sum(
95
110
  cell["points"]
@@ -97,9 +112,19 @@ class FastAPINotebookBuilder:
97
112
  if cell["question"] == cell_dict["question"]
98
113
  )
99
114
 
100
- first_cell_header = ["max_question_points = " + str(max_question_points) + "\n"]
115
+ first_cell_header = [f"max_question_points = str({max_question_points})\n"]
101
116
  first_cell_header.append("earned_points = 0 \n")
102
117
  first_cell_header.append("os.environ['EARNED_POINTS'] = str(earned_points)\n")
118
+ first_cell_header.append(
119
+ f"os.environ['TOTAL_POINTS_FREE_RESPONSE'] = str({self.total_points})\n"
120
+ )
121
+
122
+ short_filename = self.filename.split(".")[0].replace("_temp", "")
123
+ first_cell_header.extend(
124
+ [
125
+ f'log_variable("total-points",f"{self.assignment_tag}, {short_filename}", {self.total_points})\n'
126
+ ]
127
+ )
103
128
 
104
129
  return first_cell_header
105
130
 
@@ -21,12 +21,14 @@ class NotebookProcessor:
21
21
 
22
22
  Attributes:
23
23
  root_folder (str): The root directory containing notebooks to process.
24
+ assignment_tag (str): Tag for the assignment being processed.
24
25
  solutions_folder (str): The directory where processed notebooks and solutions are stored.
25
26
  verbose (bool): Flag for verbose output to the console.
26
27
  log (bool): Flag to enable or disable logging.
27
28
  """
28
29
 
29
30
  root_folder: str
31
+ assignment_tag: str = field(default="")
30
32
  solutions_folder: str = field(init=False)
31
33
  verbose: bool = False
32
34
  log: bool = True
@@ -63,6 +65,8 @@ class NotebookProcessor:
63
65
  ) # Create a logger instance specific to this module
64
66
  self.logger = logger # Assign the logger instance to the class for use in instance methods
65
67
 
68
+ self.total_point_log = {}
69
+
66
70
  def process_notebooks(self):
67
71
  """
68
72
  Recursively processes Jupyter notebooks in a given folder and its subfolders.
@@ -117,6 +121,12 @@ class NotebookProcessor:
117
121
  # Process the notebook if it meets the criteria
118
122
  self._process_single_notebook(notebook_path)
119
123
 
124
+ # Write the dictionary to a JSON file
125
+ with open(f"{self.solutions_folder}/total_points.json", "w") as json_file:
126
+ json.dump(
127
+ self.total_point_log, json_file, indent=4
128
+ ) # `indent=4` for pretty formatting
129
+
120
130
  def _print_and_log(self, message):
121
131
  """
122
132
  Logs a message and optionally prints it to the console.
@@ -169,6 +179,11 @@ class NotebookProcessor:
169
179
  None
170
180
  """
171
181
 
182
+ self.select_many_total_points = 0
183
+ self.mcq_total_points = 0
184
+ self.tf_total_points = 0
185
+ self.otter_total_points = 0
186
+
172
187
  print(f"Processing notebook: {notebook_path}")
173
188
 
174
189
  logging.info(f"Processing notebook: {notebook_path}")
@@ -213,7 +228,7 @@ class NotebookProcessor:
213
228
  if any([solution_path_1, solution_path_2, solution_path_3]) is not None:
214
229
  solution_path = solution_path_1 or solution_path_2 or solution_path_3
215
230
 
216
- student_notebook = self.free_response_parser(
231
+ student_notebook, self.otter_total_points = self.free_response_parser(
217
232
  temp_notebook_path, notebook_subfolder, notebook_name
218
233
  )
219
234
 
@@ -276,11 +291,32 @@ class NotebookProcessor:
276
291
  os.path.join(questions_folder_jbook, question_file_name_sanitized),
277
292
  )
278
293
 
294
+ total_points = (
295
+ self.select_many_total_points
296
+ + self.mcq_total_points
297
+ + self.tf_total_points
298
+ + self.otter_total_points
299
+ )
300
+
301
+ self.total_point_log.update({notebook_name: total_points})
302
+
279
303
  def free_response_parser(
280
304
  self, temp_notebook_path, notebook_subfolder, notebook_name
281
305
  ):
282
306
  if self.has_assignment(temp_notebook_path, "# ASSIGNMENT CONFIG"):
283
307
  # TODO: This is hardcoded for now, but should be in a configuration file.
308
+ client_private_key = os.path.join(
309
+ os.path.dirname(temp_notebook_path),
310
+ ".client_private_key.bin",
311
+ )
312
+ server_public_key = os.path.join(
313
+ os.path.dirname(temp_notebook_path),
314
+ ".server_public_key.bin",
315
+ )
316
+
317
+ shutil.copy("./keys/.client_private_key.bin", client_private_key)
318
+ shutil.copy("./keys/.server_public_key.bin", server_public_key)
319
+
284
320
  client_private_key = os.path.join(
285
321
  notebook_subfolder,
286
322
  ".client_private_key.bin",
@@ -293,7 +329,9 @@ class NotebookProcessor:
293
329
  shutil.copy("./keys/.client_private_key.bin", client_private_key)
294
330
  shutil.copy("./keys/.server_public_key.bin", server_public_key)
295
331
 
296
- FastAPINotebookBuilder(notebook_path=temp_notebook_path)
332
+ out = FastAPINotebookBuilder(
333
+ notebook_path=temp_notebook_path, assignment_tag=self.assignment_tag
334
+ )
297
335
 
298
336
  debug_notebook = os.path.join(
299
337
  notebook_subfolder,
@@ -338,10 +376,10 @@ class NotebookProcessor:
338
376
  os.remove(client_private_key)
339
377
  os.remove(server_public_key)
340
378
 
341
- return student_notebook
379
+ return student_notebook, out.total_points
342
380
  else:
343
381
  NotebookProcessor.add_initialization_code(temp_notebook_path)
344
- return None
382
+ return None, 0
345
383
 
346
384
  @staticmethod
347
385
  def remove_assignment_config_cells(notebook_path):
@@ -390,7 +428,9 @@ class NotebookProcessor:
390
428
 
391
429
  for data_ in data:
392
430
  # Generate the solution file
393
- self.generate_solution_MCQ(data, output_file=solution_path)
431
+ self.mcq_total_points = self.generate_solution_MCQ(
432
+ data, output_file=solution_path
433
+ )
394
434
 
395
435
  question_path = (
396
436
  f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
@@ -430,7 +470,9 @@ class NotebookProcessor:
430
470
 
431
471
  # for data_ in data:
432
472
  # Generate the solution file
433
- self.generate_solution_MCQ(data, output_file=solution_path)
473
+ self.tf_total_points = self.generate_solution_MCQ(
474
+ data, output_file=solution_path
475
+ )
434
476
 
435
477
  question_path = f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
436
478
 
@@ -466,7 +508,9 @@ class NotebookProcessor:
466
508
 
467
509
  # for data_ in data:
468
510
  # Generate the solution file
469
- self.generate_solution_MCQ(data, output_file=solution_path)
511
+ self.select_many_total_points = self.generate_solution_MCQ(
512
+ data, output_file=solution_path
513
+ )
470
514
 
471
515
  question_path = f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
472
516
 
@@ -673,17 +717,19 @@ class NotebookProcessor:
673
717
  if hasattr(existing_module, "total_points"):
674
718
  total_points.extend(existing_module.total_points)
675
719
 
720
+ question_points = 0
676
721
  # Process new question data and update solutions and total_points
677
722
  for question_set in data_list:
678
723
  for key, question_data in question_set.items():
679
724
  solution_key = f"q{question_data['question number']}-{question_data['subquestion_number']}-{key}"
680
725
  solutions[solution_key] = question_data["solution"]
681
726
  total_points.extend([question_data["points"]])
727
+ question_points += question_data["points"]
682
728
 
683
729
  # Write updated total_points and solutions back to the file
684
730
  with open(output_file, "w", encoding="utf-8") as f:
685
731
  f.write("from typing import Any\n\n")
686
- f.write(f"total_points: float = {total_points}\n\n")
732
+ f.write(f"total_points: list[float] = {total_points}\n\n")
687
733
 
688
734
  f.write("solutions: dict[str, Any] = {\n")
689
735
  for key, solution in solutions.items():
@@ -691,6 +737,8 @@ class NotebookProcessor:
691
737
  f.write(f' "{key}": {repr(solution)},\n')
692
738
  f.write("}\n")
693
739
 
740
+ return question_points
741
+
694
742
  def extract_MCQ(ipynb_file):
695
743
  """
696
744
  Extracts questions from markdown cells and organizes them as a nested dictionary,
@@ -1637,9 +1685,18 @@ def main():
1637
1685
  parser.add_argument(
1638
1686
  "root_folder", type=str, help="Path to the root folder to process"
1639
1687
  )
1640
- args = parser.parse_args()
1641
1688
 
1642
- processor = NotebookProcessor(args.root_folder)
1689
+ parser.add_argument(
1690
+ "--assignment-tag",
1691
+ type=str,
1692
+ help="assignment-tag used for calculating grades",
1693
+ default="Reading-Week-X",
1694
+ )
1695
+
1696
+ args = parser.parse_args()
1697
+ processor = NotebookProcessor(
1698
+ root_folder=args.root_folder, assignment_tag=args.assignment_tag
1699
+ )
1643
1700
  processor.process_notebooks()
1644
1701
 
1645
1702
 
@@ -6,7 +6,7 @@ import panel as pn
6
6
  import requests
7
7
  from IPython import get_ipython
8
8
 
9
- from .telemetry import ensure_responses, telemetry, update_responses
9
+ from .telemetry import ensure_responses, log_variable, telemetry, update_responses
10
10
 
11
11
 
12
12
  def initialize_assignment(
@@ -50,6 +50,8 @@ def initialize_assignment(
50
50
  update_responses(key="assignment", value=name)
51
51
  update_responses(key="jhub_user", value=jhub_user)
52
52
 
53
+ log_variable("Student Info", jhub_user, seed)
54
+
53
55
  responses = ensure_responses()
54
56
  # TODO: Add more checks here?
55
57
  assert isinstance(responses.get("seed"), int), "Seed not set"
@@ -0,0 +1,214 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from .parse import LogParser\n",
10
+ "\n",
11
+ "# ----------------- Example usage -----------------\n",
12
+ "\n",
13
+ "if __name__ == \"__main__\":\n",
14
+ " log_lines = [\n",
15
+ " \"Student Info, 449, jovyan, 2024-12-27 20:55:12\",\n",
16
+ " \"total-points, 4.0, week1-readings, 17_operators_q, 2024-12-27 20:55:23\",\n",
17
+ " \"17_operators_q, question-operators-mario-dining-1, 0, 0.5, 2024-12-27 20:55:23\",\n",
18
+ " \"17_operators_q, question-operators-mario-dining-2, 0, 0.5, 2024-12-27 20:55:23\",\n",
19
+ " \"17_operators_q, question-operators-mario-dining-3, 0, 0.5, 2024-12-27 20:55:23\",\n",
20
+ " \"17_operators_q, question-operators-mario-dining-4, 0, 0.5, 2024-12-27 20:55:23\",\n",
21
+ " \"17_operators_q, question-operators-mario-dining-5, 0, 1.0, 2024-12-27 20:55:23\",\n",
22
+ " \"total-points, 4.0, week1-readings, 17_operators_q, 2024-12-27 20:55:42\",\n",
23
+ " \"17_operators_q, question-operators-mario-dining-1, 0.5, 0.5, 2024-12-27 20:55:42\",\n",
24
+ " \"17_operators_q, question-operators-mario-dining-2, 0.5, 0.5, 2024-12-27 20:55:42\",\n",
25
+ " \"17_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:42\",\n",
26
+ " \"17_operators_q, question-operators-mario-dining-4, 0.5, 0.5, 2024-12-27 20:55:42\",\n",
27
+ " \"17_operators_q, question-operators-mario-dining-5, 1.0, 1.0, 2024-12-27 20:55:42\",\n",
28
+ " \"total-points, 2.0, week1-readings, 17_operators_q, 2024-12-27 20:55:47\",\n",
29
+ " \"17_operators_q, question-operators-mario-dining-1, 0.5, 0.5, 2024-12-27 20:55:47\",\n",
30
+ " \"17_operators_q, question-operators-mario-dining-2, 0, 0.5, 2024-12-27 20:55:47\",\n",
31
+ " \"17_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:47\",\n",
32
+ " \"17_operators_q, question-operators-mario-dining-4, 0, 0.5, 2024-12-27 20:55:47\",\n",
33
+ " \"17_operators_q, question-operators-mario-dining-5, 0, 1.0, 2024-12-27 20:55:47\",\n",
34
+ " \"19_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:47\",\n",
35
+ " ]\n",
36
+ "\n",
37
+ " parser = LogParser(log_lines=log_lines, week_tag=\"week1-readings\")\n",
38
+ " parser.parse_logs()\n",
39
+ " parser.calculate_total_scores()\n",
40
+ " results = parser.get_results()\n",
41
+ "\n",
42
+ " print(\"Student Information:\")\n",
43
+ " print(results[\"student_information\"])\n",
44
+ "\n",
45
+ " print(\"\\nAssignment Information:\")\n",
46
+ " for assignment, info in results[\"assignment_information\"].items():\n",
47
+ " print(f\"\\nAssignment Tag: {assignment}\")\n",
48
+ " print(f\"Latest Timestamp: {info['latest_timestamp']}\")\n",
49
+ " print(f\"Total Score: {info['total_score']}\")\n",
50
+ " print(f\"Max Points: {info['max_points']}\")\n",
51
+ "\n",
52
+ " print(\"\\nAssignment Scores:\")\n",
53
+ " for assignment, score_info in results[\"assignment_scores\"].items():\n",
54
+ " print(f\"\\nAssignment Tag: {assignment}\")\n",
55
+ " print(f\"Total Score Earned: {score_info['total_score']}\")\n",
56
+ " print(\"Questions:\")\n",
57
+ " for q_tag, q_data in score_info[\"questions\"].items():\n",
58
+ " print(f\" {q_tag}:\")\n",
59
+ " print(f\" score_earned: {q_data['score_earned']}\")\n",
60
+ " print(f\" score_possible: {q_data['score_possible']}\")\n",
61
+ " print(f\" timestamp: {q_data['timestamp']}\")"
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": null,
67
+ "metadata": {},
68
+ "outputs": [],
69
+ "source": [
70
+ "log_lines = [\n",
71
+ " # Student Info\n",
72
+ " \"Student Info, 449, jovyan, 2024-12-27 20:55:12\",\n",
73
+ " # Week 1 Assignment: 17_operators_q\n",
74
+ " \"total-points, 3.0, week1-readings, 17_operators_q, 2024-12-27 20:55:23\",\n",
75
+ " \"17_operators_q, question-operators-mario-dining-1, 0, 0.5, 2024-12-27 20:55:23\",\n",
76
+ " \"17_operators_q, question-operators-mario-dining-2, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
77
+ " \"17_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
78
+ " \"17_operators_q, question-operators-mario-dining-4, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
79
+ " \"17_operators_q, question-operators-mario-dining-5, 1.0, 1.0, 2024-12-27 20:55:23\",\n",
80
+ " # Week 1 Assignment: 18_advanced_q\n",
81
+ " \"total-points, 4.0, week1-readings, 18_advanced_q, 2024-12-27 20:56:00\",\n",
82
+ " \"18_advanced_q, question-advanced-problem-1, 1.0, 1.0, 2024-12-27 20:56:00\",\n",
83
+ " \"18_advanced_q, question-advanced-problem-2, 1.0, 1.0, 2024-12-27 20:56:00\",\n",
84
+ " \"18_advanced_q, question-advanced-problem-3, 0.5, 1.0, 2024-12-27 20:56:00\",\n",
85
+ " \"18_advanced_q, question-advanced-problem-4, 0.5, 1.0, 2024-12-27 20:56:00\",\n",
86
+ " # Week 2 Assignment: 19_concepts_q\n",
87
+ " \"total-points, 5.0, week2-concepts, 19_concepts_q, 2024-12-28 20:57:00\",\n",
88
+ " \"19_concepts_q, question-concepts-basic-1, 0.5, 1.0, 2024-12-28 20:57:00\",\n",
89
+ " \"19_concepts_q, question-concepts-basic-2, 0.5, 1.0, 2024-12-28 20:57:00\",\n",
90
+ " \"19_concepts_q, question-concepts-basic-3, 0.5, 1.0, 2024-12-28 20:57:00\",\n",
91
+ " \"19_concepts_q, question-concepts-basic-4, 0.5, 1.0, 2024-12-28 20:57:00\",\n",
92
+ " \"19_concepts_q, question-concepts-basic-5, 1.0, 1.0, 2024-12-28 20:57:00\",\n",
93
+ "]\n",
94
+ "\n",
95
+ "\n",
96
+ "parser = LogParser(log_lines=log_lines, week_tag=\"week1-readings\")\n",
97
+ "parser.parse_logs()\n",
98
+ "parser.calculate_total_scores()\n",
99
+ "results = parser.get_results()\n",
100
+ "\n",
101
+ "results"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "code",
106
+ "execution_count": null,
107
+ "metadata": {},
108
+ "outputs": [],
109
+ "source": [
110
+ "log_lines = [\n",
111
+ " # Student Info\n",
112
+ " \"Student Info, 550, jovyan2, 2024-12-27 20:55:12\",\n",
113
+ " # Week 1 Assignment: 17_operators_q (Initial Attempt)\n",
114
+ " \"total-points, 3.0, week1-readings,17_operators_q, 2024-12-27 20:55:23\",\n",
115
+ " \"17_operators_q, question-operators-mario-dining-1, 0, 0.5, 2024-12-27 20:55:23\",\n",
116
+ " \"17_operators_q, question-operators-mario-dining-2, 0, 0.5, 2024-12-27 20:55:23\",\n",
117
+ " \"17_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
118
+ " \"17_operators_q, question-operators-mario-dining-4, 0, 0.5, 2024-12-27 20:55:23\",\n",
119
+ " \"17_operators_q, question-operators-mario-dining-5, 0, 1.0, 2024-12-27 20:55:23\",\n",
120
+ " # Week 1 Assignment: 17_operators_q (Re-attempt)\n",
121
+ " \"total-points, 3.0, week1-readings,17_operators_q, 2024-12-27 21:00:00\",\n",
122
+ " \"17_operators_q, question-operators-mario-dining-1, 0.5, 0.5, 2024-12-27 21:00:00\",\n",
123
+ " \"17_operators_q, question-operators-mario-dining-2, 0.5, 0.5, 2024-12-27 21:00:00\",\n",
124
+ " \"17_operators_q, question-operators-mario-dining-4, 0.5, 0.5, 2024-12-27 21:00:00\",\n",
125
+ " # Week 1 Assignment: 18_challenging_q\n",
126
+ " \"total-points, 5.0, week1-readings,18_challenging_q, 2024-12-27 21:05:00\",\n",
127
+ " \"18_challenging_q, question-challenging-problem-1, 1.0, 1.0, 2024-12-27 21:05:00\",\n",
128
+ " \"18_challenging_q, question-challenging-problem-2, 0.5, 1.0, 2024-12-27 21:05:00\",\n",
129
+ " \"18_challenging_q, question-challenging-problem-3, 1.0, 1.0, 2024-12-27 21:05:00\",\n",
130
+ " \"18_challenging_q, question-challenging-problem-4, 1.0, 1.0, 2024-12-27 21:05:00\",\n",
131
+ " \"18_challenging_q, question-challenging-problem-5, 0, 1.0, 2024-12-27 21:05:00\",\n",
132
+ "]\n",
133
+ "\n",
134
+ "parser = LogParser(log_lines=log_lines, week_tag=\"week1-readings\")\n",
135
+ "parser.parse_logs()\n",
136
+ "parser.calculate_total_scores()\n",
137
+ "results = parser.get_results()\n",
138
+ "\n",
139
+ "results"
140
+ ]
141
+ },
142
+ {
143
+ "cell_type": "code",
144
+ "execution_count": null,
145
+ "metadata": {},
146
+ "outputs": [],
147
+ "source": [
148
+ "log_lines = [\n",
149
+ " # Student Info\n",
150
+ " \"Student Info, 660, jovyan3, 2024-12-27 20:55:12\",\n",
151
+ " # Week 1 Assignment: skipped some questions\n",
152
+ " \"total-points, 4.0, week1-readings,17_operators_q, 2024-12-27 20:55:23\",\n",
153
+ " \"17_operators_q, question-operators-mario-dining-1, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
154
+ " \"17_operators_q, question-operators-mario-dining-3, 0.5, 0.5, 2024-12-27 20:55:23\",\n",
155
+ " \"17_operators_q, question-operators-mario-dining-5, 1.0, 1.0, 2024-12-27 20:55:23\",\n",
156
+ " \"total-points, 4.0, week1-readings,18_operators_q, 2024-12-27 20:55:23\",\n",
157
+ " # Week 2 Assignment: all questions attempted\n",
158
+ " \"total-points, 5.0, week2-math,20_math_q, 2024-12-28 20:55:23\",\n",
159
+ " \"20_math_q, question-math-basic-1, 1.0, 1.0, 2024-12-28 20:55:23\",\n",
160
+ " \"20_math_q, question-math-basic-2, 0.5, 1.0, 2024-12-28 20:55:23\",\n",
161
+ " \"20_math_q, question-math-basic-3, 0.5, 1.0, 2024-12-28 20:55:23\",\n",
162
+ " \"20_math_q, question-math-basic-4, 1.0, 1.0, 2024-12-28 20:55:23\",\n",
163
+ " \"20_math_q, question-math-basic-5, 0.5, 1.0, 2024-12-28 20:55:23\",\n",
164
+ " # Week 3 Assignment: some skipped, partial scores\n",
165
+ " \"total-points, 4.0, week3-concepts,21_concepts_q, 2024-12-29 20:55:23\",\n",
166
+ " \"21_concepts_q, question-concepts-basic-1, 0.5, 1.0, 2024-12-29 20:55:23\",\n",
167
+ " \"21_concepts_q, question-concepts-basic-2, 0.5, 1.0, 2024-12-29 20:55:23\",\n",
168
+ "]\n",
169
+ "\n",
170
+ "parser = LogParser(log_lines=log_lines, week_tag=\"week1-readings\")\n",
171
+ "parser.parse_logs()\n",
172
+ "parser.calculate_total_scores()\n",
173
+ "results = parser.get_results()\n",
174
+ "\n",
175
+ "results"
176
+ ]
177
+ },
178
+ {
179
+ "cell_type": "code",
180
+ "execution_count": null,
181
+ "metadata": {},
182
+ "outputs": [],
183
+ "source": []
184
+ },
185
+ {
186
+ "cell_type": "code",
187
+ "execution_count": null,
188
+ "metadata": {},
189
+ "outputs": [],
190
+ "source": []
191
+ }
192
+ ],
193
+ "metadata": {
194
+ "kernelspec": {
195
+ "display_name": "engr131_dev",
196
+ "language": "python",
197
+ "name": "python3"
198
+ },
199
+ "language_info": {
200
+ "codemirror_mode": {
201
+ "name": "ipython",
202
+ "version": 3
203
+ },
204
+ "file_extension": ".py",
205
+ "mimetype": "text/x-python",
206
+ "name": "python",
207
+ "nbconvert_exporter": "python",
208
+ "pygments_lexer": "ipython3",
209
+ "version": "3.12.7"
210
+ }
211
+ },
212
+ "nbformat": 4,
213
+ "nbformat_minor": 2
214
+ }
@@ -0,0 +1,184 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List, Optional
3
+
4
+
5
+ @dataclass
6
+ class LogParser:
7
+ """
8
+ A class for parsing chronological logs and extracting information.
9
+ Handles both assignment info and question-level details.
10
+ """
11
+
12
+ log_lines: List[str]
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)
16
+
17
+ def parse_logs(self):
18
+ """
19
+ Main method to parse logs and populate student_info and assignments.
20
+ """
21
+ unique_students = set()
22
+
23
+ self._find_all_questions()
24
+
25
+ for line in reversed(
26
+ self.log_lines
27
+ ): # Process in reverse to get the most recent entries first
28
+ if self._is_student_info(line):
29
+ self._process_student_info(line, unique_students)
30
+ elif (
31
+ any(item in line for item in self.all_questions)
32
+ and "total-points" in line
33
+ ):
34
+ self._process_assignment_header(line)
35
+
36
+ # process assignment entries after all headers have been processed
37
+ for line in reversed(self.log_lines):
38
+ if (
39
+ any(item in line for item in self.all_questions)
40
+ and "total-points" not in line
41
+ ):
42
+ self._process_assignment_entry(line)
43
+
44
+ def _find_all_questions(self):
45
+ """
46
+ Finds all questions in the log_lines and returns a list of them.
47
+ """
48
+ questions = []
49
+ for line in self.log_lines:
50
+ if self.week_tag in line:
51
+ parts = line.split(",")
52
+ question_tag = parts[3].strip()
53
+ if question_tag not in questions:
54
+ questions.append(question_tag)
55
+ self.all_questions = questions
56
+
57
+ def _is_student_info(self, line: str) -> bool:
58
+ """
59
+ Checks if the line contains student information.
60
+ """
61
+ return line.startswith("Student Info")
62
+
63
+ def _process_student_info(self, line: str, unique_students: set):
64
+ """
65
+ Processes a line containing student information.
66
+ Raises an error if multiple unique students are found.
67
+ """
68
+ parts = line.split(", ")
69
+ # Example: "Student Info, 790, jovyan, 2024-12-27 19:40:10"
70
+ student_name = parts[2].strip()
71
+ unique_students.add(student_name)
72
+
73
+ if len(unique_students) > 1:
74
+ raise ValueError(
75
+ f"Error: Multiple unique student names found: {unique_students}"
76
+ )
77
+
78
+ # Only set student_info once
79
+ if not self.student_info:
80
+ self.student_info = {
81
+ "student_id": parts[1].strip(),
82
+ "username": student_name,
83
+ "timestamp": parts[3].strip(),
84
+ }
85
+
86
+ def _process_assignment_header(self, line: str):
87
+ parts = line.split(",")
88
+ assignment_tag = parts[0].strip()
89
+ if assignment_tag.startswith("total-points"):
90
+ # Handle total-points lines as assignment info
91
+ total_points_value = self._extract_total_points(parts)
92
+ timestamp = parts[-1].strip()
93
+ notebook_name = parts[3].strip()
94
+
95
+ if notebook_name not in self.assignments:
96
+ self.assignments[notebook_name] = {
97
+ "max_points": total_points_value,
98
+ "notebook": notebook_name,
99
+ "assignment": self.week_tag,
100
+ "total_score": 0.0,
101
+ "latest_timestamp": timestamp,
102
+ "questions": {}, # Ensure 'questions' key is initialized
103
+ }
104
+ elif self.assignments[notebook_name]["latest_timestamp"] < timestamp:
105
+ self.assignments[notebook_name]["max_points"] = total_points_value
106
+ self.assignments[notebook_name]["latest_timestamp"] = timestamp
107
+
108
+ def _process_assignment_entry(self, line: str):
109
+ """
110
+ Processes a line containing an assignment entry.
111
+ Adds it to the assignments dictionary.
112
+ """
113
+ parts = line.split(",")
114
+ assignment_tag = parts[0].strip()
115
+ question_tag = parts[1].strip()
116
+ score_earned = float(parts[2].strip()) if len(parts) > 2 else 0.0
117
+ score_possible = float(parts[3].strip()) if len(parts) > 3 else 0.0
118
+ timestamp = parts[-1].strip()
119
+
120
+ # Ensure assignment entry exists
121
+ if assignment_tag not in self.assignments:
122
+ self.assignments[assignment_tag] = {
123
+ "questions": {},
124
+ "total_score": 0.0,
125
+ "latest_timestamp": timestamp,
126
+ }
127
+
128
+ # Add or update the question with the most recent timestamp
129
+ questions = self.assignments[assignment_tag]["questions"]
130
+ if (
131
+ question_tag not in questions
132
+ or timestamp > questions[question_tag]["timestamp"]
133
+ ):
134
+ questions[question_tag] = {
135
+ "score_earned": score_earned,
136
+ "score_possible": score_possible,
137
+ "timestamp": timestamp,
138
+ }
139
+
140
+ # Update the latest timestamp if this one is more recent
141
+ if timestamp > self.assignments[assignment_tag]["latest_timestamp"]:
142
+ self.assignments[assignment_tag]["latest_timestamp"] = timestamp
143
+
144
+ def _extract_total_points(self, parts: List[str]) -> Optional[float]:
145
+ """
146
+ Extracts the total-points value from the parts array of a total-points line.
147
+ """
148
+ try:
149
+ return float(parts[1].strip())
150
+ except (ValueError, IndexError):
151
+ return None
152
+
153
+ def calculate_total_scores(self):
154
+ """
155
+ Calculates total scores for each assignment by summing the 'score_earned'
156
+ of its questions, and sets 'total_points' if it was not specified.
157
+ """
158
+ for assignment, data in self.assignments.items():
159
+ # Sum of all question score_earned
160
+ total_score = sum(q["score_earned"] for q in data["questions"].values())
161
+ data["total_score"] = total_score
162
+
163
+ def get_results(self) -> Dict[str, Dict]:
164
+ """
165
+ Returns the parsed results as a hierarchical dictionary with three sections:
166
+ """
167
+ return {
168
+ "student_information": self.student_info,
169
+ "assignment_information": {
170
+ assignment: {
171
+ "latest_timestamp": data["latest_timestamp"],
172
+ "total_score": data["total_score"],
173
+ "max_points": data.get("max_points", 0.0),
174
+ }
175
+ for assignment, data in self.assignments.items()
176
+ },
177
+ "assignment_scores": {
178
+ assignment: {
179
+ "questions": data["questions"],
180
+ "total_score": data["total_score"],
181
+ }
182
+ for assignment, data in self.assignments.items()
183
+ },
184
+ }
@@ -11,8 +11,28 @@ from IPython.core.interactiveshell import ExecutionInfo
11
11
  from requests import Response
12
12
  from requests.auth import HTTPBasicAuth
13
13
 
14
- # Set logging config (`force` is important)
15
- logging.basicConfig(filename=".output.log", level=logging.INFO, force=True)
14
+ # Logger for .output_code.log
15
+ logger_code = logging.getLogger("code_logger")
16
+ logger_code.setLevel(logging.INFO)
17
+
18
+ file_handler_code = logging.FileHandler(".output_code.log")
19
+ 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
+ logger_code.addHandler(file_handler_code)
25
+
26
+ # Logger for .output_reduced.log
27
+ logger_reduced = logging.getLogger("reduced_logger")
28
+ logger_reduced.setLevel(logging.INFO)
29
+
30
+ file_handler_reduced = logging.FileHandler(".output_reduced.log")
31
+ file_handler_reduced.setLevel(logging.INFO)
32
+
33
+ # file_handler_reduced.setFormatter(formatter)
34
+
35
+ logger_reduced.addHandler(file_handler_reduced)
16
36
 
17
37
  #
18
38
  # Local functions
@@ -51,20 +71,30 @@ def ensure_responses() -> dict:
51
71
  return responses
52
72
 
53
73
 
54
- def log_encrypted(message: str) -> None:
74
+ def log_encrypted(logger: logging.Logger, message: str) -> None:
75
+ """
76
+ Logs an encrypted version of the given message using the provided logger.
77
+
78
+ Args:
79
+ logger (object): The logger object used to log the encrypted message.
80
+ message (str): The message to be encrypted and logged.
81
+
82
+ Returns:
83
+ None
84
+ """
55
85
  encrypted_b64 = encrypt_to_b64(message)
56
- logging.info(f"Encrypted Output: {encrypted_b64}")
86
+ logger.info(f"Encrypted Output: {encrypted_b64}")
57
87
 
58
88
 
59
89
  def log_variable(assignment_name, value, info_type) -> None:
60
90
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
61
- message = f"{info_type}, {value}, {timestamp}, {assignment_name}"
62
- log_encrypted(message)
91
+ message = f"{assignment_name}, {info_type}, {value}, {timestamp}"
92
+ log_encrypted(logger_reduced, message)
63
93
 
64
94
 
65
95
  def telemetry(info: ExecutionInfo) -> None:
66
96
  cell_content = info.raw_cell
67
- log_encrypted(f"code run: {cell_content}")
97
+ log_encrypted(logger_code, f"code run: {cell_content}")
68
98
 
69
99
 
70
100
  def update_responses(key: str, value) -> dict:
@@ -32,28 +32,7 @@ def validate_logfile(
32
32
  # Generate box from private and public keys
33
33
  key_box = generate_keys()
34
34
 
35
- with open(filepath, "r") as logfile:
36
- encrypted_lines = logfile.readlines()
37
-
38
- decrypted_log: list[str] = []
39
- for line in encrypted_lines:
40
- if "Encrypted Output: " in line:
41
- trimmed = line.split("Encrypted Output: ")[1].strip()
42
- decoded = base64.b64decode(trimmed)
43
- decrypted = key_box.decrypt(decoded).decode()
44
- decrypted_log.append(decrypted)
45
-
46
- # Decoding the log file
47
- # data_: list[str] = drexel_jupyter_logger.decode_log_file(self.filepath, key=key)
48
- # _loginfo = str(decrypted_log)
49
-
50
- # Where possible, we should work with this reduced list of relevant entries
51
- # Here we take only lines with student info or question scores
52
- log_reduced = [
53
- entry
54
- for entry in decrypted_log
55
- if re.match(r"info,", entry) or re.match(r"q\d+_\d+,", entry)
56
- ]
35
+ decrypted_log, log_reduced = read_logfile(filepath, key_box)
57
36
 
58
37
  # For debugging; to be commented out
59
38
  # with open(".output_reduced.log", "w") as f:
@@ -246,6 +225,36 @@ def validate_logfile(
246
225
  submission_message(response)
247
226
 
248
227
 
228
+ def read_logfile(filepath, key_box=None) -> tuple[list[str], list[str]]:
229
+ if key_box is None:
230
+ key_box = generate_keys()
231
+
232
+ with open(filepath, "r") as logfile:
233
+ encrypted_lines = logfile.readlines()
234
+
235
+ decrypted_log: list[str] = []
236
+ for line in encrypted_lines:
237
+ if "Encrypted Output: " in line:
238
+ trimmed = line.split("Encrypted Output: ")[1].strip()
239
+ decoded = base64.b64decode(trimmed)
240
+ decrypted = key_box.decrypt(decoded).decode()
241
+ decrypted_log.append(decrypted)
242
+
243
+ # Decoding the log file
244
+ # data_: list[str] = drexel_jupyter_logger.decode_log_file(self.filepath, key=key)
245
+ # _loginfo = str(decrypted_log)
246
+
247
+ # Where possible, we should work with this reduced list of relevant entries
248
+ # Here we take only lines with student info or question scores
249
+ log_reduced = [
250
+ entry
251
+ for entry in decrypted_log
252
+ if re.match(r"info,", entry) or re.match(r"q\d+_\d+,", entry)
253
+ ]
254
+
255
+ return decrypted_log, log_reduced
256
+
257
+
249
258
  #
250
259
  # Helper functions
251
260
  #
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes