PyKubeGrader 0.1.11__tar.gz → 0.1.13__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. {pykubegrader-0.1.11/src/PyKubeGrader.egg-info → pykubegrader-0.1.13}/PKG-INFO +1 -1
  2. {pykubegrader-0.1.11 → pykubegrader-0.1.13/src/PyKubeGrader.egg-info}/PKG-INFO +1 -1
  3. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/build/api_notebook_builder.py +18 -16
  4. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/build/build_folder.py +36 -6
  5. pykubegrader-0.1.13/src/pykubegrader/initialize.py +106 -0
  6. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/telemetry.py +6 -7
  7. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/validate.py +2 -2
  8. pykubegrader-0.1.11/src/pykubegrader/initialize.py +0 -68
  9. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/.coveragerc +0 -0
  10. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/.github/workflows/main.yml +0 -0
  11. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/.gitignore +0 -0
  12. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/.readthedocs.yml +0 -0
  13. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/AUTHORS.rst +0 -0
  14. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/CHANGELOG.rst +0 -0
  15. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/CONTRIBUTING.rst +0 -0
  16. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/LICENSE.txt +0 -0
  17. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/README.rst +0 -0
  18. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/Makefile +0 -0
  19. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
  20. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
  21. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/_static/custom.css +0 -0
  22. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/authors.rst +0 -0
  23. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/changelog.rst +0 -0
  24. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/conf.py +0 -0
  25. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/contributing.rst +0 -0
  26. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/index.rst +0 -0
  27. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/license.rst +0 -0
  28. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/readme.rst +0 -0
  29. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/docs/requirements.txt +0 -0
  30. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/examples/.responses.json +0 -0
  31. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/examples/true_false.ipynb +0 -0
  32. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/pyproject.toml +0 -0
  33. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/setup.cfg +0 -0
  34. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/setup.py +0 -0
  35. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/SOURCES.txt +0 -0
  36. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
  37. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
  38. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
  39. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/requires.txt +0 -0
  40. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
  41. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/__init__.py +0 -0
  42. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/utils.py +0 -0
  43. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/__init__.py +0 -0
  44. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/multiple_choice.py +0 -0
  45. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/reading_question.py +0 -0
  46. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/select_many.py +0 -0
  47. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/student_info.py +0 -0
  48. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/style.py +0 -0
  49. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/true_false.py +0 -0
  50. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets/types_question.py +0 -0
  51. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets_base/__init__.py +0 -0
  52. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets_base/multi_select.py +0 -0
  53. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets_base/reading.py +0 -0
  54. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/src/pykubegrader/widgets_base/select.py +0 -0
  55. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/tests/conftest.py +0 -0
  56. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/tests/import_test.py +0 -0
  57. {pykubegrader-0.1.11 → pykubegrader-0.1.13}/tox.ini +0 -0
@@ -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,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,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(
@@ -0,0 +1,106 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ import panel as pn
6
+ import requests
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.
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.
24
+
25
+ Returns:
26
+ dict: The responses dictionary after initialization.
27
+
28
+ Raises:
29
+ Exception: If the environment is unsupported or initialization fails.
30
+ """
31
+
32
+ ipython = get_ipython()
33
+ if ipython is None:
34
+ raise Exception("Setup unsuccessful. Are you in a Jupyter environment?")
35
+
36
+ try:
37
+ move_dotfiles()
38
+ ipython.events.register("pre_run_cell", telemetry)
39
+ except Exception as e:
40
+ raise Exception(f"Failed to register telemetry: {e}")
41
+
42
+ jhub_user = os.getenv("JUPYTERHUB_USER")
43
+ if jhub_user is None:
44
+ raise Exception("Setup unsuccessful. Are you on JupyterHub?")
45
+
46
+ try:
47
+ seed = hash(jhub_user) % 1000
48
+ update_responses(key="seed", value=seed)
49
+
50
+ update_responses(key="assignment", value=name)
51
+ update_responses(key="jhub_user", value=jhub_user)
52
+
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)
58
+
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}")
69
+
70
+ print("Assignment successfully initialized")
71
+ if verbose:
72
+ print(f"Assignment: {name}")
73
+ print(f"Username: {jhub_user}")
74
+
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}")
@@ -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,
@@ -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
 
@@ -1,68 +0,0 @@
1
- import json
2
- import os
3
- from typing import Optional
4
-
5
- import panel as pn
6
- from IPython import get_ipython
7
- import requests
8
- from .telemetry import telemetry, update_responses, ensure_responses
9
-
10
-
11
-
12
- def initialize_assignment(name: str,
13
- verbose: Optional[bool] = False,
14
- url: Optional[str] = "https://engr-131-api.eastus.cloudapp.azure.com/") -> None:
15
-
16
- ipython = get_ipython()
17
- if ipython is None:
18
- print("Setup unsuccessful. Are you in a Jupyter environment?")
19
- return
20
-
21
- try:
22
- ipython.events.register("pre_run_cell", telemetry)
23
- except TypeError as e:
24
- print(f"Failed to register telemetry: {e}")
25
- return
26
-
27
- jhub_user = os.getenv("JUPYTERHUB_USER")
28
- if jhub_user is None:
29
- print("Setup unsuccessful. Are you on JupyterHub?")
30
- return
31
-
32
- try:
33
- seed = hash(jhub_user) % 1000
34
- update_responses(key="seed", value=seed)
35
- update_responses(key="assignment", value=name)
36
- update_responses(key="jhub_user", value=jhub_user)
37
-
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"
48
-
49
- pn.extension(silent=True)
50
-
51
- if verbose:
52
- print("Assignment successfully initialized")
53
- print(f"Assignment: {name}")
54
- print(f"Username: {jhub_user}")
55
-
56
-
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
- return responses
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes