PyKubeGrader 0.2.12__py3-none-any.whl → 0.2.14__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.2.12
3
+ Version: 0.2.14
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,11 +1,11 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
- pykubegrader/initialize.py,sha256=DUen_B1cHFOmwiMpB_wkCqHIjvGChsu62-WiFkKa5Wo,3766
2
+ pykubegrader/initialize.py,sha256=lJu0h-fMUMRxZ70rFAEXix3H6j-TcRaxveC2SIKTzF4,3903
3
3
  pykubegrader/telemetry.py,sha256=h8PPrXWGFgPWCcrChJo2woqd_XIPMFfYcxgLJ0CWpH8,5360
4
4
  pykubegrader/utils.py,sha256=T3GYnLnTL9VXjTZNPr00sUgMgobQYsNTGwynMyXdvHk,696
5
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=Ahec6o48wHYpsLViUWi5qIHTirZQg9PJk3N4ABiZD-w,78871
8
+ pykubegrader/build/build_folder.py,sha256=8RTyr6nTHAzbHGPqb9zcQmenOgPdF78ylPG6IRAfl3c,79735
9
9
  pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
10
10
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
11
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
@@ -13,6 +13,7 @@ pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YB
13
13
  pykubegrader/log_parser/parse.ipynb,sha256=H1CUuqiUSE-tiZV1IS7VG6HAEvoopGd6i_5QM5CwoE0,12230
14
14
  pykubegrader/log_parser/parse.py,sha256=YCs_OCnoxQKsL55MjTZWXBBBsehJL8PIB9ANnC-aE44,7379
15
15
  pykubegrader/submit/submit_assignment.py,sha256=p-v31479342q0AO3lJrd8Fwk7iJQUz183OeGeiNcXOY,3405
16
+ pykubegrader/tokens/tokens.py,sha256=cKPs63aYwzvl-uRtogOL3kvy2W_GREOV_PTR5di9U6g,1406
16
17
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
17
18
  pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
18
19
  pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
@@ -25,9 +26,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
25
26
  pykubegrader/widgets_base/multi_select.py,sha256=Cl0IN21wXLZuFu-zC65aS9tD4jMfzCRJ2DPjHao5_Ak,4044
26
27
  pykubegrader/widgets_base/reading.py,sha256=_vjUPynqmJe_R4vf-7hVhGnQR726S9GL6qT8bflBXBM,5383
27
28
  pykubegrader/widgets_base/select.py,sha256=Fw3uFNOIWo1a3CvlzSx23bvi6bSmA3TqutuRbhD4Dp8,2525
28
- PyKubeGrader-0.2.12.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
29
- PyKubeGrader-0.2.12.dist-info/METADATA,sha256=DH3IMdYk9nFa_JwDfNdMSZRbr_vZy5jx7OvX8Rjy_FY,2779
30
- PyKubeGrader-0.2.12.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
31
- PyKubeGrader-0.2.12.dist-info/entry_points.txt,sha256=UPMdTT46fQwTYJWtrUwIWIbXbwyOPfNQgBFRa0frWzw,138
32
- PyKubeGrader-0.2.12.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
33
- PyKubeGrader-0.2.12.dist-info/RECORD,,
29
+ PyKubeGrader-0.2.14.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
30
+ PyKubeGrader-0.2.14.dist-info/METADATA,sha256=VzXTZ6hT1t0xMjXeOjapWfNR14PKiE1YAQaYuWKelEI,2779
31
+ PyKubeGrader-0.2.14.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
32
+ PyKubeGrader-0.2.14.dist-info/entry_points.txt,sha256=UPMdTT46fQwTYJWtrUwIWIbXbwyOPfNQgBFRa0frWzw,138
33
+ PyKubeGrader-0.2.14.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
34
+ PyKubeGrader-0.2.14.dist-info/RECORD,,
@@ -1,7 +1,6 @@
1
1
  ### Note
2
2
 
3
3
 
4
-
5
4
  import argparse
6
5
  import importlib.util
7
6
  import json
@@ -28,6 +27,7 @@ import nbformat
28
27
  from .api_notebook_builder import FastAPINotebookBuilder
29
28
  from typing import Optional
30
29
 
30
+
31
31
  @dataclass
32
32
  class NotebookProcessor:
33
33
  """
@@ -160,22 +160,21 @@ class NotebookProcessor:
160
160
 
161
161
  if self.check_if_file_in_folder("assignment_config.yaml"):
162
162
  self.add_assignment()
163
-
163
+
164
164
  self.update_initialize_function()
165
-
165
+
166
166
  def update_initialize_function(self):
167
-
167
+
168
168
  for key, value in self.total_point_log.items():
169
-
169
+
170
170
  assignment_tag = f"week{self.week_num}-{self.assignment_type}"
171
-
171
+
172
172
  update_initialize_assignment(
173
- notebook_path = os.path.join(self.root_folder, key + '.ipynb'),
174
- assignment_points= value,
175
- assignment_tag = assignment_tag,
176
- )
177
-
178
-
173
+ notebook_path=os.path.join(self.root_folder, key + ".ipynb"),
174
+ assignment_points=value,
175
+ assignment_tag=assignment_tag,
176
+ )
177
+
179
178
  def build_payload(self, yaml_content):
180
179
  """
181
180
  Reads YAML content for an assignment and returns Python variables.
@@ -215,15 +214,15 @@ class NotebookProcessor:
215
214
  "due_date": due_date,
216
215
  "max_score": int(self.assignment_total_points),
217
216
  }
218
-
219
- def build_payload_notebook(self, yaml_content, notebook_title, total_points):
217
+
218
+ def build_payload_notebook(self, yaml_content, notebook_title, total_points):
220
219
  # Parse the YAML content
221
220
  with open(yaml_content, "r") as file:
222
221
  data = yaml.safe_load(file)
223
-
222
+
224
223
  # Extract assignment details
225
224
  assignment = data.get("assignment", {})
226
-
225
+
227
226
  week_num = self.week_num
228
227
  assignment_type = self.assignment_type
229
228
  due_date_str = assignment.get("due_date")
@@ -235,7 +234,7 @@ class NotebookProcessor:
235
234
  due_date = parser.parse(due_date_str) # Automatically handles timezones
236
235
  except ValueError as e:
237
236
  print(f"Error parsing due_date: {e}")
238
-
237
+
239
238
  return {
240
239
  "title": notebook_title,
241
240
  "week_number": week_num,
@@ -243,8 +242,7 @@ class NotebookProcessor:
243
242
  "due_date": due_date,
244
243
  "max_score": total_points,
245
244
  }
246
-
247
-
245
+
248
246
  def add_notebook(self, notebook_title, total_points):
249
247
  """
250
248
  Sends a POST request to add a notebook.
@@ -253,9 +251,11 @@ class NotebookProcessor:
253
251
  url = "https://engr-131-api.eastus.cloudapp.azure.com/notebook"
254
252
 
255
253
  # Build the payload
256
- payload = self.build_payload_notebook(yaml_content=f"{self.root_folder}/assignment_config.yaml",
257
- notebook_title=notebook_title,
258
- total_points=total_points)
254
+ payload = self.build_payload_notebook(
255
+ yaml_content=f"{self.root_folder}/assignment_config.yaml",
256
+ notebook_title=notebook_title,
257
+ total_points=total_points,
258
+ )
259
259
 
260
260
  # Define HTTP Basic Authentication
261
261
  auth = (user(), password())
@@ -496,19 +496,50 @@ class NotebookProcessor:
496
496
  self.select_many_total_points
497
497
  + self.mcq_total_points
498
498
  + self.tf_total_points
499
- + self.otter_total_points
499
+ + self.otter_total_points
500
500
  )
501
-
501
+
502
502
  # creates the assignment record in the database
503
503
  self.add_notebook(notebook_name, total_points)
504
504
 
505
505
  self.assignment_total_points += total_points
506
506
 
507
507
  self.total_point_log.update({notebook_name: total_points})
508
-
509
- student_file_path = os.path.join(self.root_folder, notebook_name + '.ipynb')
508
+
509
+ student_file_path = os.path.join(self.root_folder, notebook_name + ".ipynb")
510
510
  self.add_submission_cells(student_file_path, student_file_path)
511
-
511
+ NotebookProcessor.remove_empty_cells(student_file_path)
512
+
513
+ @staticmethod
514
+ def remove_empty_cells(notebook_path, output_path=None):
515
+ """
516
+ Removes empty cells from a Jupyter Notebook and saves the updated notebook.
517
+
518
+ Parameters:
519
+ notebook_path (str): Path to the input Jupyter Notebook.
520
+ output_path (str): Path to save the updated Jupyter Notebook. If None, it overwrites the original file.
521
+ """
522
+ try:
523
+ # Load the notebook
524
+ with open(notebook_path, "r") as nb_file:
525
+ notebook = nbformat.read(nb_file, as_version=4)
526
+
527
+ # Filter out empty cells
528
+ non_empty_cells = [cell for cell in notebook.cells if cell.source.strip()]
529
+
530
+ # Update the notebook cells
531
+ notebook.cells = non_empty_cells
532
+
533
+ # Save the updated notebook
534
+ save_path = output_path if output_path else notebook_path
535
+ with open(save_path, "w") as nb_file:
536
+ nbformat.write(notebook, nb_file)
537
+
538
+ print(f"Empty cells removed. Updated notebook saved at: {save_path}")
539
+
540
+ except Exception as e:
541
+ print(f"An error occurred: {e}")
542
+
512
543
  def add_submission_cells(self, notebook_path: str, output_path: str) -> None:
513
544
  """
514
545
  Adds submission cells to the end of a Jupyter notebook.
@@ -533,12 +564,10 @@ class NotebookProcessor:
533
564
  "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
534
565
  f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
535
566
  )
536
-
567
+
537
568
  # Make the code cell non-editable and non-deletable
538
- code_cell.metadata = {
539
- "editable": False,
540
- "deletable": False
541
- }
569
+ code_cell.metadata = {"editable": False, "deletable": False}
570
+ code_cell.metadata["tags"] = ["skip-execution"]
542
571
 
543
572
  # Add the cells to the notebook
544
573
  notebook.cells.append(markdown_cell)
@@ -548,7 +577,6 @@ class NotebookProcessor:
548
577
  with open(output_path, "w", encoding="utf-8") as f:
549
578
  nbformat.write(notebook, f)
550
579
 
551
-
552
580
  def free_response_parser(
553
581
  self, temp_notebook_path, notebook_subfolder, notebook_name
554
582
  ):
@@ -783,7 +811,7 @@ class NotebookProcessor:
783
811
  return solution_path, question_path
784
812
  else:
785
813
  return None, None
786
-
814
+
787
815
  @staticmethod
788
816
  def replace_temp_no_otter(input_file, output_file):
789
817
  # Load the notebook
@@ -793,8 +821,8 @@ class NotebookProcessor:
793
821
  # Iterate through the cells and modify `cell.source`
794
822
  for cell in notebook.cells:
795
823
  if cell.cell_type == "code": # Only process code cells
796
- if 'responses = initialize_assignment(' in cell.source:
797
- cell.source = cell.source.replace('_temp', '')
824
+ if "responses = initialize_assignment(" in cell.source:
825
+ cell.source = cell.source.replace("_temp", "")
798
826
 
799
827
  # Save the modified notebook
800
828
  with open(output_file, "w", encoding="utf-8") as f:
@@ -823,7 +851,6 @@ class NotebookProcessor:
823
851
  cell["source"] = [
824
852
  line.replace("_temp.ipynb", ".ipynb") for line in cell["source"]
825
853
  ]
826
-
827
854
 
828
855
  # Write the updated notebook to the output file
829
856
  with open(output_file, "w", encoding="utf-8") as f:
@@ -1508,6 +1535,7 @@ def extract_MCQ(ipynb_file):
1508
1535
  print("Invalid JSON in notebook file.")
1509
1536
  return []
1510
1537
 
1538
+
1511
1539
  def check_for_heading(notebook_path, search_strings):
1512
1540
  """
1513
1541
  Checks if a Jupyter notebook contains a heading cell whose source matches any of the given strings.
@@ -1547,8 +1575,6 @@ def clean_notebook(notebook_path):
1547
1575
  cell.metadata["editable"] = cell.metadata.get("editable", False)
1548
1576
  cell.metadata["deletable"] = cell.metadata.get("deletable", False)
1549
1577
  if cell.cell_type == "code":
1550
- if "grader.check(" in cell.source:
1551
- continue
1552
1578
  cell.metadata["tags"] = cell.metadata.get("tags", [])
1553
1579
  if "skip-execution" not in cell.metadata["tags"]:
1554
1580
  cell.metadata["tags"].append("skip-execution")
@@ -1785,7 +1811,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
1785
1811
  )
1786
1812
  f.write(" def __init__(self):\n")
1787
1813
  f.write(" super().__init__(\n")
1788
- f.write(f" title=f'{q_value['question_text']}',\n")
1814
+ f.write(f' title=f"{q_value['question_text']}",\n')
1789
1815
  f.write(" style=MultiSelect,\n")
1790
1816
  f.write(
1791
1817
  f" question_number={q_value['question number']},\n"
@@ -1862,7 +1888,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
1862
1888
  )
1863
1889
  f.write(" def __init__(self):\n")
1864
1890
  f.write(" super().__init__(\n")
1865
- f.write(f" title=f'{q_value['question_text']}',\n")
1891
+ f.write(f' title=f"{q_value['question_text']}",\n')
1866
1892
  f.write(" style=TFStyle,\n")
1867
1893
  f.write(
1868
1894
  f" question_number={q_value['question number']},\n"
@@ -1953,7 +1979,8 @@ def replace_cell_source(notebook_path, cell_index, new_source):
1953
1979
  # Save the notebook
1954
1980
  with open(notebook_path, "w", encoding="utf-8") as f:
1955
1981
  nbformat.write(notebook, f)
1956
-
1982
+
1983
+
1957
1984
  def update_initialize_assignment(
1958
1985
  notebook_path: str,
1959
1986
  assignment_points: Optional[float] = None,
@@ -2001,11 +2028,11 @@ def update_initialize_assignment(
2001
2028
  existing_args = match.group(1).strip()
2002
2029
  # Replace with the updated line
2003
2030
  if additional_variables_str:
2031
+ updated_line = f"responses = initialize_assignment({existing_args}, {additional_variables_str})\n"
2032
+ else:
2004
2033
  updated_line = (
2005
- f"responses = initialize_assignment({existing_args}, {additional_variables_str})\n"
2034
+ f"responses = initialize_assignment({existing_args})\n"
2006
2035
  )
2007
- else:
2008
- updated_line = f"responses = initialize_assignment({existing_args})\n"
2009
2036
  source_code[i] = updated_line
2010
2037
  updated = True
2011
2038
 
@@ -9,7 +9,7 @@ from IPython import get_ipython
9
9
  from typing import Optional
10
10
  from .telemetry import ensure_responses, log_variable, telemetry, update_responses
11
11
 
12
-
12
+ #TODO: could cleanup to remove redundant imports
13
13
  def initialize_assignment(
14
14
  name: str,
15
15
  week: str,
@@ -33,6 +33,9 @@ def initialize_assignment(
33
33
  Raises:
34
34
  Exception: If the environment is unsupported or initialization fails.
35
35
  """
36
+
37
+ if assignment_tag is None:
38
+ assignment_tag = f"{week}-{assignment_type}"
36
39
 
37
40
  ipython = get_ipython()
38
41
  if ipython is None:
@@ -0,0 +1,53 @@
1
+ import requests
2
+ import os
3
+ import json
4
+
5
+
6
+ try:
7
+ from pykubegrader.build.passwords import password, user
8
+ except: # noqa: E722
9
+ print("Passwords not found, cannot access database")
10
+
11
+
12
+ def build_token_payload(token: str, duration: int) -> dict:
13
+
14
+ if os.getenv("JUPYTERHUB_USER", None) is None:
15
+ raise ValueError("JupyterHub user not found")
16
+
17
+ # Return the extracted details as a dictionary
18
+ return {
19
+ "value": token,
20
+ "requester": os.getenv("JUPYTERHUB_USER", None),
21
+ "duration": duration,
22
+ }
23
+
24
+
25
+ # Need to do for add token as well
26
+ def add_token(token, duration=20):
27
+ """
28
+ Sends a POST request to add a notebook.
29
+ """
30
+ # Define the URL
31
+ url = "https://engr-131-api.eastus.cloudapp.azure.com/tokens"
32
+
33
+ # Build the payload
34
+ payload = build_token_payload(token=token, duration=duration)
35
+
36
+ # Define HTTP Basic Authentication
37
+ auth = (user(), password())
38
+
39
+ # Define headers
40
+ headers = {"Content-Type": "application/json"}
41
+
42
+ # Serialize the payload with the custom JSON encoder
43
+ serialized_payload = json.dumps(payload)
44
+
45
+ # Send the POST request
46
+ response = requests.post(url, data=serialized_payload, headers=headers, auth=auth)
47
+
48
+ # Print the response
49
+ print(f"Status Code: {response.status_code}")
50
+ try:
51
+ print(f"Response: {response.json()}")
52
+ except ValueError:
53
+ print(f"Response: {response.text}")