PyKubeGrader 0.1.11__py3-none-any.whl → 0.1.13__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.11
3
+ Version: 0.1.13
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,10 +1,10 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
- pykubegrader/initialize.py,sha256=SREGdFK8kqnk8RYTtaTO5LKZiK2Y4YxBNjrALl5jaNo,1956
3
- pykubegrader/telemetry.py,sha256=Zkap_ml7hWz7akBVzE-zqri_K-AkBSTEhv0IE3VM9iY,3943
2
+ pykubegrader/initialize.py,sha256=3mz-Zv1mlmHTRNktObmOIbBRsipg16JX6wg9-_vFQe4,3043
3
+ pykubegrader/telemetry.py,sha256=4-YdfAepiKO8y9TFoU_ktI64iFTIuKK3RcAXIz0KmP0,3972
4
4
  pykubegrader/utils.py,sha256=dKw6SyRYU3DWRgD3xER7wq-C9e1daWPkqr901LpcwiQ,642
5
- pykubegrader/validate.py,sha256=PeT6Gx4ZOQYyogG4nb3TD9YfEkAmf__fR1aOOB3ZBWo,10705
6
- pykubegrader/build/api_notebook_builder.py,sha256=vtRA9lDCc-PXN-y56wEXS-h_aKXDU_ChS-1_ooiKCOk,18975
7
- pykubegrader/build/build_folder.py,sha256=m7PkiAvpC5kIcm9A2BLtTncWJo4S1fuup5ZCo35VZys,63323
5
+ pykubegrader/validate.py,sha256=F0SuGGj236rFr0HFLhuF1R1whrs2vhbDrG5qu_0PojQ,10707
6
+ pykubegrader/build/api_notebook_builder.py,sha256=DPxjBhqUmSVF3PVhvgBbZR2BaCpp6Zt9fDUFyR_CAX0,19126
7
+ pykubegrader/build/build_folder.py,sha256=Iw20V65Eggli4noS4WaNJJcYmo185A01ofxQBtODYec,64366
8
8
  pykubegrader/widgets/__init__.py,sha256=s3ky3eJDa1RedFVdpKxmqv6mHBYpOSL9Z6qThSH9cbs,303
9
9
  pykubegrader/widgets/multiple_choice.py,sha256=NjD3-uXSnibpUQ0mO3hRp_O-rynFyl0Dz6IXE4tnCRI,2078
10
10
  pykubegrader/widgets/reading_question.py,sha256=y30_swHwzH8LrT8deWTnxctAAmR8BSxTlXAqMgUrAT4,3031
@@ -17,9 +17,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
17
17
  pykubegrader/widgets_base/multi_select.py,sha256=u50IOhYxC_S_gq31VnFPLdbNajk_SUWhaqlMSJxhqVQ,3439
18
18
  pykubegrader/widgets_base/reading.py,sha256=4uTLmlPzCwxVzufFhPjM7W19uMGguRb6y4eAV3x-zAc,5314
19
19
  pykubegrader/widgets_base/select.py,sha256=h1S5StcbX8S-Wiyga4fVDhPbVvRxffwaqyVbiiuInRs,2743
20
- PyKubeGrader-0.1.11.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
21
- PyKubeGrader-0.1.11.dist-info/METADATA,sha256=as-usJZup_AKIA1HVysnVKuWs9XlZfAtWNUhFriar0s,2665
22
- PyKubeGrader-0.1.11.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
23
- PyKubeGrader-0.1.11.dist-info/entry_points.txt,sha256=Kd4Bh-i3hc4qlnLU1p0nc8yPw9cC5AQGOtkk2eLGnQw,78
24
- PyKubeGrader-0.1.11.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
25
- PyKubeGrader-0.1.11.dist-info/RECORD,,
20
+ PyKubeGrader-0.1.13.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
21
+ PyKubeGrader-0.1.13.dist-info/METADATA,sha256=GMuh1zsIM9Ft5nioXmqsLiCT9tQqdCerweyPoiq8cig,2665
22
+ PyKubeGrader-0.1.13.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
23
+ PyKubeGrader-0.1.13.dist-info/entry_points.txt,sha256=Kd4Bh-i3hc4qlnLU1p0nc8yPw9cC5AQGOtkk2eLGnQw,78
24
+ PyKubeGrader-0.1.13.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
25
+ PyKubeGrader-0.1.13.dist-info/RECORD,,
@@ -1,11 +1,12 @@
1
+ import ast
2
+ import json
3
+ import re
4
+ import shutil
1
5
  from dataclasses import dataclass
2
6
  from pathlib import Path
3
7
  from typing import Optional
4
- import json
8
+
5
9
  import nbformat
6
- import json
7
- import re
8
- import shutil
9
10
 
10
11
 
11
12
  @dataclass
@@ -20,7 +21,6 @@ class FastAPINotebookBuilder:
20
21
  self.run()
21
22
 
22
23
  def run(self):
23
-
24
24
  # here for easy debugging
25
25
  if self.temp_notebook is not None:
26
26
  shutil.copy(
@@ -34,7 +34,6 @@ class FastAPINotebookBuilder:
34
34
  self.add_api_code()
35
35
 
36
36
  def add_api_code(self):
37
-
38
37
  for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
39
38
  print(
40
39
  f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
@@ -77,7 +76,9 @@ class FastAPINotebookBuilder:
77
76
  )
78
77
  updated_cell_source.extend(["earned_points += score\n"])
79
78
  updated_cell_source.extend(
80
- [f'log_variable(f"{{score}}, {{max_score}}", question_id)\n']
79
+ [
80
+ f'log_variable(f"{{score}}, {{max_score}}", question_id, "{self.filename.split(".")[0]}")\n'
81
+ ]
81
82
  )
82
83
  updated_cell_source.extend(
83
84
  ["os.environ['EARNED_POINTS'] = str(earned_points)\n"]
@@ -105,7 +106,6 @@ class FastAPINotebookBuilder:
105
106
  @staticmethod
106
107
  def construct_update_responses(cell_dict):
107
108
  update_responses = []
108
- question_id = cell_dict["question"] + "-" + str(cell_dict["test_number"]) + "\n"
109
109
 
110
110
  logging_variables = cell_dict["logging_variables"]
111
111
 
@@ -141,7 +141,6 @@ class FastAPINotebookBuilder:
141
141
 
142
142
  @staticmethod
143
143
  def construct_graders(cell_dict):
144
-
145
144
  # Generate Python code
146
145
  added_code = [
147
146
  "if "
@@ -205,7 +204,7 @@ class FastAPINotebookBuilder:
205
204
  " ensure_responses,\n",
206
205
  " log_variable,\n",
207
206
  " score_question,\n",
208
- " submit_question_new,\n",
207
+ " submit_question,\n",
209
208
  " telemetry,\n",
210
209
  " update_responses,\n",
211
210
  ")\n",
@@ -283,7 +282,7 @@ class FastAPINotebookBuilder:
283
282
  """
284
283
  last_import_index = -1
285
284
  is_multiline_import = False # Flag to track if we're inside a multiline import
286
-
285
+
287
286
  for i, line in enumerate(cell_source):
288
287
  stripped_line = line.strip()
289
288
 
@@ -313,11 +312,15 @@ class FastAPINotebookBuilder:
313
312
  if "source" in cell:
314
313
  for line in cell["source"]:
315
314
  # Look for the log_variables pattern
316
- match = re.search(r"log_variables:\s*\[(.*?)\]", line)
315
+ match = re.search(r"log_variables:\s*(\[.*\])", line)
317
316
  if match:
318
- # Split the variables by comma and strip whitespace
319
- log_variables = [var.strip() for var in match.group(1).split(",")]
320
- return log_variables
317
+ # Parse the list using ast.literal_eval for safety
318
+ try:
319
+ log_variables = ast.literal_eval(match.group(1))
320
+ if isinstance(log_variables, list):
321
+ return [var.strip() for var in log_variables]
322
+ except (SyntaxError, ValueError):
323
+ pass
321
324
  return []
322
325
 
323
326
  def tag_questions(cells_dict):
@@ -363,7 +366,6 @@ class FastAPINotebookBuilder:
363
366
  return cells_dict
364
367
 
365
368
  def question_dict(self):
366
-
367
369
  notebook_path = Path(self.temp_notebook)
368
370
  if not notebook_path.exists():
369
371
  raise FileNotFoundError(f"The file {notebook_path} does not exist.")
@@ -8,7 +8,9 @@ import shutil
8
8
  import subprocess
9
9
  import sys
10
10
  from dataclasses import dataclass, field
11
+
11
12
  import nbformat
13
+
12
14
  from .api_notebook_builder import FastAPINotebookBuilder
13
15
 
14
16
 
@@ -278,26 +280,38 @@ class NotebookProcessor:
278
280
  self, temp_notebook_path, notebook_subfolder, notebook_name
279
281
  ):
280
282
  if self.has_assignment(temp_notebook_path, "# ASSIGNMENT CONFIG"):
281
-
282
283
  # TODO: This is hardcoded for now, but should be in a configuration file.
283
284
  client_private_key = os.path.join(
284
285
  notebook_subfolder,
285
- "client_private_key.bin",
286
+ ".client_private_key.bin",
286
287
  )
287
288
  server_public_key = os.path.join(
288
289
  notebook_subfolder,
289
- "server_public_key.bin",
290
+ ".server_public_key.bin",
290
291
  )
291
292
 
292
- shutil.copy("./keys/client_private_key.bin", client_private_key)
293
- shutil.copy("./keys/server_public_key.bin", server_public_key)
293
+ shutil.copy("./keys/.client_private_key.bin", client_private_key)
294
+ shutil.copy("./keys/.server_public_key.bin", server_public_key)
294
295
 
295
296
  FastAPINotebookBuilder(notebook_path=temp_notebook_path)
296
297
 
298
+ debug_notebook = os.path.join(
299
+ notebook_subfolder,
300
+ "dist",
301
+ "autograder",
302
+ os.path.basename(temp_notebook_path).replace("_temp", "_debugger"),
303
+ )
304
+
297
305
  self.run_otter_assign(
298
306
  temp_notebook_path, os.path.join(notebook_subfolder, "dist")
299
307
  )
300
308
 
309
+ print(f"Copying {temp_notebook_path} to {debug_notebook}")
310
+
311
+ shutil.copy(temp_notebook_path, debug_notebook)
312
+
313
+ NotebookProcessor.remove_assignment_config_cells(debug_notebook)
314
+
301
315
  student_notebook = os.path.join(
302
316
  notebook_subfolder, "dist", "student", f"{notebook_name}.ipynb"
303
317
  )
@@ -329,6 +343,23 @@ class NotebookProcessor:
329
343
  NotebookProcessor.add_initialization_code(temp_notebook_path)
330
344
  return None
331
345
 
346
+ @staticmethod
347
+ def remove_assignment_config_cells(notebook_path):
348
+ # Read the notebook
349
+ with open(notebook_path, "r", encoding="utf-8") as f:
350
+ notebook = nbformat.read(f, as_version=nbformat.NO_CONVERT)
351
+
352
+ # Filter out cells containing "# ASSIGNMENT CONFIG"
353
+ notebook.cells = [
354
+ cell
355
+ for cell in notebook.cells
356
+ if "# ASSIGNMENT CONFIG" not in cell.get("source", "")
357
+ ]
358
+
359
+ # Save the updated notebook
360
+ with open(notebook_path, "w", encoding="utf-8") as f:
361
+ nbformat.write(notebook, f)
362
+
332
363
  @staticmethod
333
364
  def add_initialization_code(notebook_path):
334
365
  # finds the first code cell
@@ -340,7 +371,6 @@ class NotebookProcessor:
340
371
  replace_cell_source(notebook_path, index, cell)
341
372
 
342
373
  def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
343
-
344
374
  ### Parse the notebook for multiple choice questions
345
375
  if self.has_assignment(temp_notebook_path, "# BEGIN MULTIPLE CHOICE"):
346
376
  self._print_and_log(
@@ -1,68 +1,106 @@
1
- import json
2
1
  import os
3
- from typing import Optional
2
+ import shutil
3
+ from pathlib import Path
4
4
 
5
5
  import panel as pn
6
- from IPython import get_ipython
7
6
  import requests
8
- from .telemetry import telemetry, update_responses, ensure_responses
7
+ from IPython import get_ipython
8
+
9
+ from .telemetry import ensure_responses, telemetry, update_responses
10
+
11
+
12
+ def initialize_assignment(
13
+ name: str,
14
+ url: str = "https://engr-131-api.eastus.cloudapp.azure.com/",
15
+ verbose: bool = False,
16
+ ) -> dict:
17
+ """
18
+ Initialize an assignment in a Jupyter environment.
9
19
 
20
+ Args:
21
+ name (str): The name of the assignment.
22
+ url (str): The URL of the API server.
23
+ verbose (bool): Whether to print detailed initialization information.
10
24
 
25
+ Returns:
26
+ dict: The responses dictionary after initialization.
11
27
 
12
- def initialize_assignment(name: str,
13
- verbose: Optional[bool] = False,
14
- url: Optional[str] = "https://engr-131-api.eastus.cloudapp.azure.com/") -> None:
28
+ Raises:
29
+ Exception: If the environment is unsupported or initialization fails.
30
+ """
15
31
 
16
32
  ipython = get_ipython()
17
33
  if ipython is None:
18
- print("Setup unsuccessful. Are you in a Jupyter environment?")
19
- return
34
+ raise Exception("Setup unsuccessful. Are you in a Jupyter environment?")
20
35
 
21
36
  try:
37
+ move_dotfiles()
22
38
  ipython.events.register("pre_run_cell", telemetry)
23
- except TypeError as e:
24
- print(f"Failed to register telemetry: {e}")
25
- return
39
+ except Exception as e:
40
+ raise Exception(f"Failed to register telemetry: {e}")
26
41
 
27
42
  jhub_user = os.getenv("JUPYTERHUB_USER")
28
43
  if jhub_user is None:
29
- print("Setup unsuccessful. Are you on JupyterHub?")
30
- return
44
+ raise Exception("Setup unsuccessful. Are you on JupyterHub?")
31
45
 
32
46
  try:
33
47
  seed = hash(jhub_user) % 1000
34
48
  update_responses(key="seed", value=seed)
49
+
35
50
  update_responses(key="assignment", value=name)
36
51
  update_responses(key="jhub_user", value=jhub_user)
37
52
 
38
- except (TypeError, json.JSONDecodeError) as e:
39
- print(f"Failed to initialize assignment: {e}")
40
- return
41
-
42
-
43
- # extract responses
44
- responses = ensure_responses()
45
-
46
- # TODO: Add more checks here??
47
- assert isinstance(responses.get('seed'), int), "valid seed not found in responses"
53
+ responses = ensure_responses()
54
+ # TODO: Add more checks here?
55
+ assert isinstance(responses.get("seed"), int), "Seed not set"
56
+
57
+ pn.extension(silent=True)
48
58
 
49
- pn.extension(silent=True)
59
+ # Check connection to API server
60
+ params = {"jhub_user": responses["jhub_user"]}
61
+ response = requests.get(url, params=params)
62
+ if verbose:
63
+ print(f"status code: {response.status_code}")
64
+ data = response.json()
65
+ for k, v in data.items():
66
+ print(f"{k}: {v}")
67
+ except Exception as e:
68
+ raise Exception(f"Failed to initialize assignment: {e}")
50
69
 
70
+ print("Assignment successfully initialized")
51
71
  if verbose:
52
- print("Assignment successfully initialized")
53
72
  print(f"Assignment: {name}")
54
73
  print(f"Username: {jhub_user}")
55
-
56
74
 
57
-
58
- # Checks connectivity to the API
59
- params = { "jhub_user": responses["jhub_user"] }
60
- response = requests.get(url, params=params)
61
- if verbose:
62
- print(f"status code: {response.status_code}")
63
- data = response.json()
64
- for k, v in data.items():
65
- print(f"{k}: {v}")
66
-
67
- print("Assignment successfully initialized")
68
75
  return responses
76
+
77
+
78
+ #
79
+ # Helper functions
80
+ #
81
+
82
+
83
+ def move_dotfiles():
84
+ """
85
+ Move essential dotfiles from a fixed source directory to the current working directory.
86
+
87
+ Raises:
88
+ FileNotFoundError: If a source file is missing.
89
+ Exception: If copying fails for any other reason.
90
+ """
91
+ source_dir = Path("/opt/dotfiles")
92
+ target_dir = Path.cwd()
93
+
94
+ files_to_copy = [".client_private_key.bin", ".server_public_key.bin"]
95
+
96
+ for file_name in files_to_copy:
97
+ source_file = source_dir / file_name
98
+ target_file = target_dir / file_name
99
+
100
+ if not source_file.exists():
101
+ raise FileNotFoundError(f"Key file not found: {source_file}")
102
+
103
+ try:
104
+ shutil.copy2(source_file, target_file)
105
+ except Exception as e:
106
+ raise Exception(f"Failed to copy {source_file} to {target_file}: {e}")
pykubegrader/telemetry.py CHANGED
@@ -20,11 +20,11 @@ logging.basicConfig(filename=".output.log", level=logging.INFO, force=True)
20
20
 
21
21
 
22
22
  def encrypt_to_b64(message: str) -> str:
23
- with open("server_public_key.bin", "rb") as f:
23
+ with open(".server_public_key.bin", "rb") as f:
24
24
  server_pub_key_bytes = f.read()
25
25
  server_pub_key = nacl.public.PublicKey(server_pub_key_bytes)
26
26
 
27
- with open("client_private_key.bin", "rb") as f:
27
+ with open(".client_private_key.bin", "rb") as f:
28
28
  client_private_key_bytes = f.read()
29
29
  client_priv_key = nacl.public.PrivateKey(client_private_key_bytes)
30
30
 
@@ -36,7 +36,6 @@ def encrypt_to_b64(message: str) -> str:
36
36
 
37
37
 
38
38
  def ensure_responses() -> dict:
39
-
40
39
  with open(".responses.json", "a") as _:
41
40
  pass
42
41
 
@@ -48,7 +47,7 @@ def ensure_responses() -> dict:
48
47
  except json.JSONDecodeError:
49
48
  with open(".responses.json", "w") as f:
50
49
  json.dump(responses, f)
51
-
50
+
52
51
  return responses
53
52
 
54
53
 
@@ -57,9 +56,9 @@ def log_encrypted(message: str) -> None:
57
56
  logging.info(f"Encrypted Output: {encrypted_b64}")
58
57
 
59
58
 
60
- def log_variable(value, info_type) -> None:
59
+ def log_variable(assignment_name, value, info_type) -> None:
61
60
  timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
62
- message = f"{info_type}, {value}, {timestamp}"
61
+ message = f"{info_type}, {value}, {timestamp}, {assignment_name}"
63
62
  log_encrypted(message)
64
63
 
65
64
 
@@ -120,7 +119,7 @@ def score_question(
120
119
  return res
121
120
 
122
121
 
123
- def submit_question_new(
122
+ def submit_question(
124
123
  student_email: str,
125
124
  term: str,
126
125
  assignment: str,
pykubegrader/validate.py CHANGED
@@ -252,11 +252,11 @@ def validate_logfile(
252
252
 
253
253
 
254
254
  def generate_keys() -> nacl.public.Box:
255
- with open("server_private_key.bin", "rb") as priv_file:
255
+ with open(".server_private_key.bin", "rb") as priv_file:
256
256
  server_private_key_bytes = priv_file.read()
257
257
  server_priv_key = nacl.public.PrivateKey(server_private_key_bytes)
258
258
 
259
- with open("client_public_key.bin", "rb") as pub_file:
259
+ with open(".client_public_key.bin", "rb") as pub_file:
260
260
  client_public_key_bytes = pub_file.read()
261
261
  client_pub_key = nacl.public.PublicKey(client_public_key_bytes)
262
262