PyKubeGrader 0.1.23__tar.gz → 0.2.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/.gitignore +4 -2
- {pykubegrader-0.1.23/src/PyKubeGrader.egg-info → pykubegrader-0.2.0}/PKG-INFO +3 -1
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/setup.cfg +3 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0/src/PyKubeGrader.egg-info}/PKG-INFO +3 -1
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/SOURCES.txt +1 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/entry_points.txt +1 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/requires.txt +2 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/build/build_folder.py +153 -42
- pykubegrader-0.2.0/src/pykubegrader/build/clean_folder.py +45 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/log_parser/parse.ipynb +1 -1
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/validate.py +4 -2
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/.coveragerc +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/.readthedocs.yml +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/AUTHORS.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/CHANGELOG.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/LICENSE.txt +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/README.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/Makefile +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/_static/custom.css +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/authors.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/changelog.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/conf.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/contributing.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/index.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/license.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/readme.rst +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/docs/requirements.txt +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/examples/.responses.json +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/examples/true_false.ipynb +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/pyproject.toml +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/setup.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/build/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/build/api_notebook_builder.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/graders/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/graders/late_assignments.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/initialize.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/log_parser/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/log_parser/parse.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/telemetry.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/select_many.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/style.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/true_false.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/multi_select.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/reading.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/src/pykubegrader/widgets_base/select.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/tests/conftest.py +0 -0
- {pykubegrader-0.1.23 → pykubegrader-0.2.0}/tests/import_test.py +0 -0
- {pykubegrader-0.1.23 → 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.
|
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
|
@@ -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.
|
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
|
@@ -40,6 +40,7 @@ src/pykubegrader/validate.py
|
|
40
40
|
src/pykubegrader/build/__init__.py
|
41
41
|
src/pykubegrader/build/api_notebook_builder.py
|
42
42
|
src/pykubegrader/build/build_folder.py
|
43
|
+
src/pykubegrader/build/clean_folder.py
|
43
44
|
src/pykubegrader/graders/__init__.py
|
44
45
|
src/pykubegrader/graders/late_assignments.py
|
45
46
|
src/pykubegrader/log_parser/__init__.py
|
@@ -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
|
@@ -58,11 +84,6 @@ class NotebookProcessor:
|
|
58
84
|
format="%(asctime)s - %(levelname)s - %(message)s", # Log message format: timestamp, level, and message
|
59
85
|
)
|
60
86
|
|
61
|
-
self.assignmet_type = self.assignment_tag.split("-")[0].lower()
|
62
|
-
|
63
|
-
week_num = self.assignment_tag.split("-")[-1]
|
64
|
-
self.week = f"week_{week_num}"
|
65
|
-
|
66
87
|
# Initialize a global logger for the class
|
67
88
|
global logger
|
68
89
|
logger = logging.getLogger(
|
@@ -132,6 +153,84 @@ class NotebookProcessor:
|
|
132
153
|
self.total_point_log, json_file, indent=4
|
133
154
|
) # `indent=4` for pretty formatting
|
134
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
|
+
|
135
234
|
def _print_and_log(self, message):
|
136
235
|
"""
|
137
236
|
Logs a message and optionally prints it to the console.
|
@@ -220,29 +319,42 @@ class NotebookProcessor:
|
|
220
319
|
else:
|
221
320
|
self._print_and_log(f"Notebook already in destination: {new_notebook_path}")
|
222
321
|
|
223
|
-
solution_path_1,
|
322
|
+
solution_path_1, question_path_1 = self.multiple_choice_parser(
|
224
323
|
temp_notebook_path, new_notebook_path
|
225
324
|
)
|
226
|
-
solution_path_2,
|
325
|
+
solution_path_2, question_path_2 = self.true_false_parser(
|
227
326
|
temp_notebook_path, new_notebook_path
|
228
327
|
)
|
229
|
-
solution_path_3,
|
328
|
+
solution_path_3, question_path_3 = self.select_many_parser(
|
230
329
|
temp_notebook_path, new_notebook_path
|
231
330
|
)
|
232
331
|
|
233
332
|
if any([solution_path_1, solution_path_2, solution_path_3]) is not None:
|
234
333
|
solution_path = solution_path_1 or solution_path_2 or solution_path_3
|
235
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
|
+
|
236
338
|
student_notebook, self.otter_total_points = self.free_response_parser(
|
237
339
|
temp_notebook_path, notebook_subfolder, notebook_name
|
238
340
|
)
|
239
341
|
|
240
342
|
# If Otter does not run, move the student file to the main directory
|
241
343
|
if student_notebook is None:
|
344
|
+
clean_notebook(temp_notebook_path)
|
242
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
|
+
)
|
243
352
|
self._print_and_log(
|
244
353
|
f"Copied and cleaned student notebook: {path_} -> {self.root_folder}"
|
245
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
|
+
)
|
246
358
|
|
247
359
|
# Move the solution file to the autograder folder
|
248
360
|
if solution_path is not None:
|
@@ -303,6 +415,8 @@ class NotebookProcessor:
|
|
303
415
|
+ self.otter_total_points
|
304
416
|
)
|
305
417
|
|
418
|
+
self.assignment_total_points += total_points
|
419
|
+
|
306
420
|
self.total_point_log.update({notebook_name: total_points})
|
307
421
|
|
308
422
|
def free_response_parser(
|
@@ -360,11 +474,9 @@ class NotebookProcessor:
|
|
360
474
|
)
|
361
475
|
|
362
476
|
NotebookProcessor.add_initialization_code(
|
363
|
-
student_notebook, self.week, self.
|
477
|
+
student_notebook, self.week, self.assignment_type
|
364
478
|
)
|
365
479
|
|
366
|
-
self.clean_notebook(student_notebook)
|
367
|
-
|
368
480
|
NotebookProcessor.replace_temp_in_notebook(
|
369
481
|
student_notebook, student_notebook
|
370
482
|
)
|
@@ -374,6 +486,9 @@ class NotebookProcessor:
|
|
374
486
|
NotebookProcessor.replace_temp_in_notebook(
|
375
487
|
autograder_notebook, autograder_notebook
|
376
488
|
)
|
489
|
+
|
490
|
+
clean_notebook(student_notebook)
|
491
|
+
|
377
492
|
shutil.copy(student_notebook, self.root_folder)
|
378
493
|
self._print_and_log(
|
379
494
|
f"Copied and cleaned student notebook: {student_notebook} -> {self.root_folder}"
|
@@ -386,10 +501,17 @@ class NotebookProcessor:
|
|
386
501
|
return student_notebook, out.total_points
|
387
502
|
else:
|
388
503
|
NotebookProcessor.add_initialization_code(
|
389
|
-
temp_notebook_path, self.week, self.
|
504
|
+
temp_notebook_path, self.week, self.assignment_type
|
390
505
|
)
|
391
506
|
return None, 0
|
392
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
|
+
|
393
515
|
@staticmethod
|
394
516
|
def remove_assignment_config_cells(notebook_path):
|
395
517
|
# Read the notebook
|
@@ -413,8 +535,8 @@ class NotebookProcessor:
|
|
413
535
|
index, cell = find_first_code_cell(notebook_path)
|
414
536
|
cell = cell["source"]
|
415
537
|
import_text = "from pykubegrader.initialize import initialize_assignment\n"
|
416
|
-
cell = f"{import_text}\n" + cell
|
417
538
|
cell += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}", "{week}", "{assignment_type}" )\n'
|
539
|
+
cell = f"{import_text}\n" + cell
|
418
540
|
replace_cell_source(notebook_path, index, cell)
|
419
541
|
|
420
542
|
def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
|
@@ -435,15 +557,11 @@ class NotebookProcessor:
|
|
435
557
|
|
436
558
|
data = NotebookProcessor.merge_metadata(value, data)
|
437
559
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
data_, output_file=solution_path
|
442
|
-
)
|
560
|
+
self.mcq_total_points = self.generate_solution_MCQ(
|
561
|
+
data, output_file=solution_path
|
562
|
+
)
|
443
563
|
|
444
|
-
|
445
|
-
f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
|
446
|
-
)
|
564
|
+
question_path = f"{new_notebook_path.replace(".ipynb", "")}_questions.py"
|
447
565
|
|
448
566
|
generate_mcq_file(data, output_file=question_path)
|
449
567
|
|
@@ -863,13 +981,6 @@ class NotebookProcessor:
|
|
863
981
|
os.rename(old_file_path, new_file_path)
|
864
982
|
logging.info(f"Renamed: {old_file_path} -> {new_file_path}")
|
865
983
|
|
866
|
-
@staticmethod
|
867
|
-
def clean_notebook(notebook_path):
|
868
|
-
"""
|
869
|
-
Cleans a Jupyter notebook to remove unwanted cells and set cell metadata.
|
870
|
-
"""
|
871
|
-
clean_notebook(notebook_path)
|
872
|
-
|
873
984
|
|
874
985
|
def extract_raw_cells(ipynb_file, heading="# BEGIN MULTIPLE CHOICE"):
|
875
986
|
"""
|
@@ -1444,7 +1555,7 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
|
|
1444
1555
|
|
1445
1556
|
for question_dict in data_dict:
|
1446
1557
|
with open(output_file, "a", encoding="utf-8") as f:
|
1447
|
-
for i, q_value in enumerate(question_dict.
|
1558
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1448
1559
|
if i == 0:
|
1449
1560
|
# Write the MCQuestion class
|
1450
1561
|
f.write(
|
@@ -1460,7 +1571,7 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
|
|
1460
1571
|
break
|
1461
1572
|
|
1462
1573
|
keys = []
|
1463
|
-
for q_value in question_dict.
|
1574
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1464
1575
|
# Write keys
|
1465
1576
|
keys.append(
|
1466
1577
|
f"q{q_value['question number']}-{q_value['subquestion_number']}-{q_value['name']}"
|
@@ -1469,20 +1580,20 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
|
|
1469
1580
|
f.write(f" keys={keys},\n")
|
1470
1581
|
|
1471
1582
|
options = []
|
1472
|
-
for q_value in question_dict.
|
1583
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1473
1584
|
# Write options
|
1474
1585
|
options.append(q_value["OPTIONS"])
|
1475
1586
|
|
1476
1587
|
f.write(f" options={options},\n")
|
1477
1588
|
|
1478
1589
|
descriptions = []
|
1479
|
-
for q_value in question_dict.
|
1590
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1480
1591
|
# Write descriptions
|
1481
1592
|
descriptions.append(q_value["question_text"])
|
1482
1593
|
f.write(f" descriptions={descriptions},\n")
|
1483
1594
|
|
1484
1595
|
points = []
|
1485
|
-
for q_value in question_dict.
|
1596
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1486
1597
|
# Write points
|
1487
1598
|
points.append(q_value["points"])
|
1488
1599
|
|
@@ -1515,7 +1626,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1515
1626
|
|
1516
1627
|
for question_dict in data_dict:
|
1517
1628
|
with open(output_file, "a", encoding="utf-8") as f:
|
1518
|
-
for i, q_value in enumerate(question_dict.
|
1629
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1519
1630
|
if i == 0:
|
1520
1631
|
# Write the MCQuestion class
|
1521
1632
|
f.write(
|
@@ -1531,7 +1642,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1531
1642
|
break
|
1532
1643
|
|
1533
1644
|
keys = []
|
1534
|
-
for q_value in question_dict.
|
1645
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1535
1646
|
# Write keys
|
1536
1647
|
keys.append(
|
1537
1648
|
f"q{q_value['question number']}-{q_value['subquestion_number']}-{q_value['name']}"
|
@@ -1540,20 +1651,20 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1540
1651
|
f.write(f" keys={keys},\n")
|
1541
1652
|
|
1542
1653
|
descriptions = []
|
1543
|
-
for q_value in question_dict.
|
1654
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1544
1655
|
# Write descriptions
|
1545
1656
|
descriptions.append(q_value["question_text"])
|
1546
1657
|
f.write(f" descriptions={descriptions},\n")
|
1547
1658
|
|
1548
1659
|
options = []
|
1549
|
-
for q_value in question_dict.
|
1660
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1550
1661
|
# Write options
|
1551
1662
|
options.append(q_value["OPTIONS"])
|
1552
1663
|
|
1553
1664
|
f.write(f" options={options},\n")
|
1554
1665
|
|
1555
1666
|
points = []
|
1556
|
-
for q_value in question_dict.
|
1667
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1557
1668
|
# Write points
|
1558
1669
|
points.append(q_value["points"])
|
1559
1670
|
|
@@ -1592,7 +1703,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1592
1703
|
|
1593
1704
|
for question_dict in data_dict:
|
1594
1705
|
with open(output_file, "a", encoding="utf-8") as f:
|
1595
|
-
for i, q_value in enumerate(question_dict.
|
1706
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1596
1707
|
if i == 0:
|
1597
1708
|
# Write the MCQuestion class
|
1598
1709
|
f.write(
|
@@ -1608,7 +1719,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1608
1719
|
break
|
1609
1720
|
|
1610
1721
|
keys = []
|
1611
|
-
for q_value in question_dict.
|
1722
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1612
1723
|
# Write keys
|
1613
1724
|
keys.append(
|
1614
1725
|
f"q{q_value['question number']}-{q_value['subquestion_number']}-{q_value['name']}"
|
@@ -1617,13 +1728,13 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1617
1728
|
f.write(f" keys={keys},\n")
|
1618
1729
|
|
1619
1730
|
descriptions = []
|
1620
|
-
for q_value in question_dict.
|
1731
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1621
1732
|
# Write descriptions
|
1622
1733
|
descriptions.append(q_value["question_text"])
|
1623
1734
|
f.write(f" descriptions={descriptions},\n")
|
1624
1735
|
|
1625
1736
|
points = []
|
1626
|
-
for q_value in question_dict.
|
1737
|
+
for i, (q_key, q_value) in enumerate(question_dict.items()):
|
1627
1738
|
# Write points
|
1628
1739
|
points.append(q_value["points"])
|
1629
1740
|
|
@@ -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())
|
@@ -23,14 +23,16 @@ def validate_logfile(
|
|
23
23
|
username: str = "student",
|
24
24
|
password: str = "capture",
|
25
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
|
-
|
33
|
-
|
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
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|