PyKubeGrader 0.1.23__py3-none-any.whl → 0.2.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyKubeGrader
3
- Version: 0.1.23
3
+ Version: 0.2.1
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
@@ -2,14 +2,15 @@ pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
2
  pykubegrader/initialize.py,sha256=t3iSdeIcndfY8LoHBVUEZfTW6sUWHyeFLirKo4GwSQE,3328
3
3
  pykubegrader/telemetry.py,sha256=jRInaDqIpdeT7F0rxLJgO38lA-SMtmLcYE8nEAGah1Q,4922
4
4
  pykubegrader/utils.py,sha256=T3GYnLnTL9VXjTZNPr00sUgMgobQYsNTGwynMyXdvHk,696
5
- pykubegrader/validate.py,sha256=TEtbPOs6boyJUnoGGoe8faPdpacDLVQKEEWPr3rka1U,11026
5
+ pykubegrader/validate.py,sha256=2KLSB3wfFZbBh1NGgmrOV073paKAgrQz4AgA6LmCIj4,11076
6
6
  pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  pykubegrader/build/api_notebook_builder.py,sha256=FEaTj1fEsPAes7KNrPuhClEvKzLuOL3wG7Hgr7FQc-o,20083
8
- pykubegrader/build/build_folder.py,sha256=feP4newePStm5U6yYedoJezTtjAj-I1J4ExSGZgwpZA,66558
8
+ pykubegrader/build/build_folder.py,sha256=e27Ni64qpgmzg4Yu94nQieyK3IflZUQ0BvEOzwMGKMU,70729
9
+ pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
9
10
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
11
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
11
12
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
12
- pykubegrader/log_parser/parse.ipynb,sha256=F3ZWi5_AOxEnSihY0VBz4jjqo0__GggjRgFvS0QCHTg,10611
13
+ pykubegrader/log_parser/parse.ipynb,sha256=kPTo9gqhDineT7yT56P9xrQP1OKTf6Ro-iR4xlJ-or8,10610
13
14
  pykubegrader/log_parser/parse.py,sha256=uw8lxWVh0FTaWi-bVmpMBbqwTXIHlJtB9gPc1qKvm4I,7040
14
15
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
15
16
  pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
@@ -23,9 +24,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
23
24
  pykubegrader/widgets_base/multi_select.py,sha256=Cl0IN21wXLZuFu-zC65aS9tD4jMfzCRJ2DPjHao5_Ak,4044
24
25
  pykubegrader/widgets_base/reading.py,sha256=_vjUPynqmJe_R4vf-7hVhGnQR726S9GL6qT8bflBXBM,5383
25
26
  pykubegrader/widgets_base/select.py,sha256=Fw3uFNOIWo1a3CvlzSx23bvi6bSmA3TqutuRbhD4Dp8,2525
26
- PyKubeGrader-0.1.23.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
27
- PyKubeGrader-0.1.23.dist-info/METADATA,sha256=h4dCKElZvmrEDFhPykKm3cMJzdjPdCFBNXcyZ5-Fd_s,2665
28
- PyKubeGrader-0.1.23.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
29
- PyKubeGrader-0.1.23.dist-info/entry_points.txt,sha256=Kd4Bh-i3hc4qlnLU1p0nc8yPw9cC5AQGOtkk2eLGnQw,78
30
- PyKubeGrader-0.1.23.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
31
- PyKubeGrader-0.1.23.dist-info/RECORD,,
27
+ PyKubeGrader-0.2.1.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
28
+ PyKubeGrader-0.2.1.dist-info/METADATA,sha256=tY6eIv1slUtGb-qLeuoxkXxPc55sAjxqGG_f9MFf5Oo,2729
29
+ PyKubeGrader-0.2.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
30
+ PyKubeGrader-0.2.1.dist-info/entry_points.txt,sha256=UPMdTT46fQwTYJWtrUwIWIbXbwyOPfNQgBFRa0frWzw,138
31
+ PyKubeGrader-0.2.1.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
32
+ PyKubeGrader-0.2.1.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  otter-folder-builder = pykubegrader.build.build_folder:main
3
+ otter-folder-cleaner = pykubegrader.build.clean_folder:main
@@ -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, question_path = self.multiple_choice_parser(
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, question_path = self.true_false_parser(
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, question_path = self.select_many_parser(
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.assignmet_type
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.assignmet_type
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"
538
+ import_text += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}", "{week}", "{assignment_type}" )\n'
416
539
  cell = f"{import_text}\n" + cell
417
- cell += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}", "{week}", "{assignment_type}" )\n'
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
- for data_ in data:
439
- # Generate the solution file
440
- self.mcq_total_points = self.generate_solution_MCQ(
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
- question_path = (
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.values()):
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.values():
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.values():
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.values():
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.values():
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.values()):
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.values():
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.values():
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.values():
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.values():
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.values()):
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.values():
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.values():
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.values():
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())
@@ -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",
pykubegrader/validate.py CHANGED
@@ -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
- # 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