PyKubeGrader 0.1.22__tar.gz → 0.2.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/.gitignore +4 -2
  2. {pykubegrader-0.1.22/src/PyKubeGrader.egg-info → pykubegrader-0.2.0}/PKG-INFO +3 -1
  3. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/examples/true_false.ipynb +2 -2
  4. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/setup.cfg +3 -0
  5. {pykubegrader-0.1.22 → pykubegrader-0.2.0/src/PyKubeGrader.egg-info}/PKG-INFO +3 -1
  6. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/SOURCES.txt +5 -0
  7. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/entry_points.txt +1 -0
  8. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/requires.txt +2 -0
  9. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/build/api_notebook_builder.py +2 -2
  10. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/build/build_folder.py +146 -26
  11. pykubegrader-0.2.0/src/pykubegrader/build/clean_folder.py +45 -0
  12. pykubegrader-0.2.0/src/pykubegrader/graders/__init__.py +1 -0
  13. pykubegrader-0.2.0/src/pykubegrader/graders/late_assignments.py +45 -0
  14. pykubegrader-0.2.0/src/pykubegrader/log_parser/__init__.py +1 -0
  15. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/log_parser/parse.ipynb +1 -1
  16. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/log_parser/parse.py +15 -15
  17. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/telemetry.py +28 -65
  18. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/utils.py +2 -2
  19. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/validate.py +11 -9
  20. pykubegrader-0.2.0/src/pykubegrader/widgets/__init__.py +1 -0
  21. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/true_false.py +2 -2
  22. pykubegrader-0.2.0/src/pykubegrader/widgets_base/__init__.py +1 -0
  23. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/multi_select.py +3 -1
  24. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/reading.py +3 -1
  25. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/select.py +3 -1
  26. pykubegrader-0.1.22/src/pykubegrader/widgets/__init__.py +0 -19
  27. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/.coveragerc +0 -0
  28. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/.github/workflows/main.yml +0 -0
  29. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/.readthedocs.yml +0 -0
  30. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/AUTHORS.rst +0 -0
  31. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/CHANGELOG.rst +0 -0
  32. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/CONTRIBUTING.rst +0 -0
  33. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/LICENSE.txt +0 -0
  34. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/README.rst +0 -0
  35. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/Makefile +0 -0
  36. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
  37. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
  38. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/_static/custom.css +0 -0
  39. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/authors.rst +0 -0
  40. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/changelog.rst +0 -0
  41. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/conf.py +0 -0
  42. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/contributing.rst +0 -0
  43. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/index.rst +0 -0
  44. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/license.rst +0 -0
  45. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/readme.rst +0 -0
  46. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/docs/requirements.txt +0 -0
  47. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/examples/.responses.json +0 -0
  48. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/pyproject.toml +0 -0
  49. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/setup.py +0 -0
  50. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
  51. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
  52. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
  53. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/__init__.py +0 -0
  54. {pykubegrader-0.1.22/src/pykubegrader/widgets_base → pykubegrader-0.2.0/src/pykubegrader/build}/__init__.py +0 -0
  55. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/initialize.py +0 -0
  56. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/multiple_choice.py +0 -0
  57. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/reading_question.py +0 -0
  58. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/select_many.py +0 -0
  59. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/student_info.py +0 -0
  60. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/style.py +0 -0
  61. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/src/pykubegrader/widgets/types_question.py +0 -0
  62. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/tests/conftest.py +0 -0
  63. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/tests/import_test.py +0 -0
  64. {pykubegrader-0.1.22 → pykubegrader-0.2.0}/tox.ini +0 -0
@@ -27,7 +27,7 @@ tags
27
27
  *.egg
28
28
  *.eggs/
29
29
  .installed.cfg
30
- *.egg-info
30
+ *.egg-info
31
31
 
32
32
  # Unittest and coverage
33
33
  htmlcov/*
@@ -42,7 +42,7 @@ coverage.xml
42
42
  build/*
43
43
  sdist/*
44
44
  dist/*
45
- docs/api/*
45
+ docs/api/*
46
46
  docs/_rst/*
47
47
  docs/_build/*
48
48
  cover/*
@@ -55,3 +55,5 @@ MANIFEST
55
55
 
56
56
  .mypy_cache/
57
57
  .ruff_cache/
58
+ password.py
59
+ passwords.py
@@ -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
@@ -6,7 +6,7 @@
6
6
  "metadata": {},
7
7
  "outputs": [],
8
8
  "source": [
9
- "from pykubegrader.widgets.true_false import TFQuestion, TrueFalse_style"
9
+ "from pykubegrader.widgets.true_false import TFQuestion, TFStyle"
10
10
  ]
11
11
  },
12
12
  {
@@ -19,7 +19,7 @@
19
19
  " def __init__(\n",
20
20
  " self,\n",
21
21
  " title=\"Respond with True or False\",\n",
22
- " style=TrueFalse_style,\n",
22
+ " style=TFStyle,\n",
23
23
  " question_number=2,\n",
24
24
  " keys=[\"MC1\", \"MC2\", \"MC3\", \"MC4\"],\n",
25
25
  " descriptions=[\n",
@@ -34,6 +34,8 @@ install_requires =
34
34
  ruff
35
35
  setuptools
36
36
  sphinx
37
+ types-python-dateutil
38
+ types-pyyaml
37
39
  types-requests
38
40
  types-setuptools
39
41
 
@@ -52,6 +54,7 @@ testing =
52
54
  Add here console scripts like =
53
55
  console_scripts =
54
56
  otter-folder-builder = pykubegrader.build.build_folder:main
57
+ otter-folder-cleaner = pykubegrader.build.clean_folder:main
55
58
 
56
59
  [tool:pytest]
57
60
  addopts =
@@ -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
@@ -37,8 +37,13 @@ src/pykubegrader/initialize.py
37
37
  src/pykubegrader/telemetry.py
38
38
  src/pykubegrader/utils.py
39
39
  src/pykubegrader/validate.py
40
+ src/pykubegrader/build/__init__.py
40
41
  src/pykubegrader/build/api_notebook_builder.py
41
42
  src/pykubegrader/build/build_folder.py
43
+ src/pykubegrader/build/clean_folder.py
44
+ src/pykubegrader/graders/__init__.py
45
+ src/pykubegrader/graders/late_assignments.py
46
+ src/pykubegrader/log_parser/__init__.py
42
47
  src/pykubegrader/log_parser/parse.ipynb
43
48
  src/pykubegrader/log_parser/parse.py
44
49
  src/pykubegrader/widgets/__init__.py
@@ -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
@@ -9,6 +9,8 @@ requests
9
9
  ruff
10
10
  setuptools
11
11
  sphinx
12
+ types-python-dateutil
13
+ types-pyyaml
12
14
  types-requests
13
15
  types-setuptools
14
16
 
@@ -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,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
@@ -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
  """
@@ -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
- )
@@ -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
 
@@ -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
  )
@@ -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,19 +0,0 @@
1
- # Auto-generated __init__.py
2
-
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
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes