PyKubeGrader 0.2.8__tar.gz → 0.2.22__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/.gitignore +1 -0
  2. {pykubegrader-0.2.8/src/PyKubeGrader.egg-info → pykubegrader-0.2.22}/PKG-INFO +1 -1
  3. {pykubegrader-0.2.8 → pykubegrader-0.2.22/src/PyKubeGrader.egg-info}/PKG-INFO +1 -1
  4. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/SOURCES.txt +2 -0
  5. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/build/api_notebook_builder.py +6 -0
  6. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/build/build_folder.py +222 -31
  7. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/initialize.py +4 -1
  8. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/submit/submit_assignment.py +28 -9
  9. pykubegrader-0.2.22/src/pykubegrader/tokens/tokens.py +47 -0
  10. pykubegrader-0.2.22/src/pykubegrader/tokens/validate_token.py +138 -0
  11. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/.coveragerc +0 -0
  12. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/.github/workflows/main.yml +0 -0
  13. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/.readthedocs.yml +0 -0
  14. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/AUTHORS.rst +0 -0
  15. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/CHANGELOG.rst +0 -0
  16. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/CONTRIBUTING.rst +0 -0
  17. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/LICENSE.txt +0 -0
  18. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/README.rst +0 -0
  19. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/Makefile +0 -0
  20. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
  21. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
  22. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/_static/custom.css +0 -0
  23. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/authors.rst +0 -0
  24. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/changelog.rst +0 -0
  25. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/conf.py +0 -0
  26. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/contributing.rst +0 -0
  27. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/index.rst +0 -0
  28. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/license.rst +0 -0
  29. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/readme.rst +0 -0
  30. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/docs/requirements.txt +0 -0
  31. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/examples/.responses.json +0 -0
  32. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/examples/true_false.ipynb +0 -0
  33. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/pyproject.toml +0 -0
  34. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/setup.cfg +0 -0
  35. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/setup.py +0 -0
  36. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
  37. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
  38. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
  39. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/requires.txt +0 -0
  40. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
  41. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/__init__.py +0 -0
  42. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/build/__init__.py +0 -0
  43. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/build/clean_folder.py +0 -0
  44. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/graders/__init__.py +0 -0
  45. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/graders/late_assignments.py +0 -0
  46. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/log_parser/__init__.py +0 -0
  47. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/log_parser/parse.ipynb +0 -0
  48. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/log_parser/parse.py +0 -0
  49. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/telemetry.py +0 -0
  50. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/utils.py +0 -0
  51. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/validate.py +0 -0
  52. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/__init__.py +0 -0
  53. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/multiple_choice.py +0 -0
  54. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/reading_question.py +0 -0
  55. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/select_many.py +0 -0
  56. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/student_info.py +0 -0
  57. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/style.py +0 -0
  58. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/true_false.py +0 -0
  59. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets/types_question.py +0 -0
  60. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets_base/__init__.py +0 -0
  61. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets_base/multi_select.py +0 -0
  62. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets_base/reading.py +0 -0
  63. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/src/pykubegrader/widgets_base/select.py +0 -0
  64. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/tests/conftest.py +0 -0
  65. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/tests/import_test.py +0 -0
  66. {pykubegrader-0.2.8 → pykubegrader-0.2.22}/tox.ini +0 -0
@@ -57,3 +57,4 @@ MANIFEST
57
57
  .ruff_cache/
58
58
  password.py
59
59
  passwords.py
60
+ src/pykubegrader/tokens/token_test.ipynb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyKubeGrader
3
- Version: 0.2.8
3
+ Version: 0.2.22
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.2.8
3
+ Version: 0.2.22
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -47,6 +47,8 @@ src/pykubegrader/log_parser/__init__.py
47
47
  src/pykubegrader/log_parser/parse.ipynb
48
48
  src/pykubegrader/log_parser/parse.py
49
49
  src/pykubegrader/submit/submit_assignment.py
50
+ src/pykubegrader/tokens/tokens.py
51
+ src/pykubegrader/tokens/validate_token.py
50
52
  src/pykubegrader/widgets/__init__.py
51
53
  src/pykubegrader/widgets/multiple_choice.py
52
54
  src/pykubegrader/widgets/reading_question.py
@@ -14,6 +14,7 @@ class FastAPINotebookBuilder:
14
14
  notebook_path: str
15
15
  temp_notebook: Optional[str] = None
16
16
  assignment_tag: Optional[str] = ""
17
+ require_key: Optional[bool] = False
17
18
 
18
19
  def __post_init__(self):
19
20
  self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
@@ -119,6 +120,11 @@ class FastAPINotebookBuilder:
119
120
  f"os.environ['TOTAL_POINTS_FREE_RESPONSE'] = str({self.total_points})\n"
120
121
  )
121
122
 
123
+ if self.require_key:
124
+ first_cell_header.append(
125
+ "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
126
+ )
127
+
122
128
  short_filename = self.filename.split(".")[0].replace("_temp", "")
123
129
  first_cell_header.extend(
124
130
  [
@@ -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,20 @@ import nbformat
28
27
  from .api_notebook_builder import FastAPINotebookBuilder
29
28
  from typing import Optional
30
29
 
30
+
31
+ import os
32
+
33
+ os.environ["JUPYTERHUB_USER"] = "jca92"
34
+ os.environ["TOKEN"] = "token"
35
+ os.environ["DB_URL"] = "https://engr-131-api.eastus.cloudapp.azure.com/"
36
+ os.environ["keys_student"] = "capture"
37
+ os.environ["user_name_student"] = "student"
38
+
39
+ from pykubegrader.tokens.tokens import add_token
40
+
41
+ add_token("token", duration=20)
42
+
43
+
31
44
  @dataclass
32
45
  class NotebookProcessor:
33
46
  """
@@ -46,6 +59,7 @@ class NotebookProcessor:
46
59
  solutions_folder: str = field(init=False)
47
60
  verbose: bool = False
48
61
  log: bool = True
62
+ require_key: bool = False
49
63
 
50
64
  def __post_init__(self):
51
65
  """
@@ -160,22 +174,21 @@ class NotebookProcessor:
160
174
 
161
175
  if self.check_if_file_in_folder("assignment_config.yaml"):
162
176
  self.add_assignment()
163
-
177
+
164
178
  self.update_initialize_function()
165
-
179
+
166
180
  def update_initialize_function(self):
167
-
181
+
168
182
  for key, value in self.total_point_log.items():
169
-
183
+
170
184
  assignment_tag = f"week{self.week_num}-{self.assignment_type}"
171
-
185
+
172
186
  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
-
187
+ notebook_path=os.path.join(self.root_folder, key + ".ipynb"),
188
+ assignment_points=value,
189
+ assignment_tag=assignment_tag,
190
+ )
191
+
179
192
  def build_payload(self, yaml_content):
180
193
  """
181
194
  Reads YAML content for an assignment and returns Python variables.
@@ -216,6 +229,69 @@ class NotebookProcessor:
216
229
  "max_score": int(self.assignment_total_points),
217
230
  }
218
231
 
232
+ def build_payload_notebook(self, yaml_content, notebook_title, total_points):
233
+ # Parse the YAML content
234
+ with open(yaml_content, "r") as file:
235
+ data = yaml.safe_load(file)
236
+
237
+ # Extract assignment details
238
+ assignment = data.get("assignment", {})
239
+
240
+ week_num = self.week_num
241
+ assignment_type = self.assignment_type
242
+ due_date_str = assignment.get("due_date")
243
+
244
+ # Convert due_date to a datetime object if available
245
+ due_date = None
246
+ if due_date_str:
247
+ try:
248
+ due_date = parser.parse(due_date_str) # Automatically handles timezones
249
+ except ValueError as e:
250
+ print(f"Error parsing due_date: {e}")
251
+
252
+ return {
253
+ "title": notebook_title,
254
+ "week_number": week_num,
255
+ "assignment_type": assignment_type,
256
+ "due_date": due_date,
257
+ "max_score": total_points,
258
+ }
259
+
260
+ def add_notebook(self, notebook_title, total_points):
261
+ """
262
+ Sends a POST request to add a notebook.
263
+ """
264
+ # Define the URL
265
+ url = "https://engr-131-api.eastus.cloudapp.azure.com/notebook"
266
+
267
+ # Build the payload
268
+ payload = self.build_payload_notebook(
269
+ yaml_content=f"{self.root_folder}/assignment_config.yaml",
270
+ notebook_title=notebook_title,
271
+ total_points=total_points,
272
+ )
273
+
274
+ # Define HTTP Basic Authentication
275
+ auth = (user(), password())
276
+
277
+ # Define headers
278
+ headers = {"Content-Type": "application/json"}
279
+
280
+ # Serialize the payload with the custom JSON encoder
281
+ serialized_payload = json.dumps(payload, default=self.json_serial)
282
+
283
+ # Send the POST request
284
+ response = requests.post(
285
+ url, data=serialized_payload, headers=headers, auth=auth
286
+ )
287
+
288
+ # Print the response
289
+ print(f"Status Code: {response.status_code}")
290
+ try:
291
+ print(f"Response: {response.json()}")
292
+ except ValueError:
293
+ print(f"Response: {response.text}")
294
+
219
295
  def add_assignment(self):
220
296
  """
221
297
  Sends a POST request to add an assignment.
@@ -437,10 +513,95 @@ class NotebookProcessor:
437
513
  + self.otter_total_points
438
514
  )
439
515
 
516
+ # creates the assignment record in the database
517
+ self.add_notebook(notebook_name, total_points)
518
+
440
519
  self.assignment_total_points += total_points
441
520
 
442
521
  self.total_point_log.update({notebook_name: total_points})
443
522
 
523
+ student_file_path = os.path.join(self.root_folder, notebook_name + ".ipynb")
524
+ self.add_submission_cells(student_file_path, student_file_path)
525
+ NotebookProcessor.remove_empty_cells(student_file_path)
526
+
527
+ @staticmethod
528
+ def remove_empty_cells(notebook_path, output_path=None):
529
+ """
530
+ Removes empty cells from a Jupyter Notebook and saves the updated notebook.
531
+
532
+ Parameters:
533
+ notebook_path (str): Path to the input Jupyter Notebook.
534
+ output_path (str): Path to save the updated Jupyter Notebook. If None, it overwrites the original file.
535
+ """
536
+ try:
537
+ # Load the notebook
538
+ with open(notebook_path, "r") as nb_file:
539
+ notebook = nbformat.read(nb_file, as_version=4)
540
+
541
+ # Filter out empty cells
542
+ non_empty_cells = [cell for cell in notebook.cells if cell.source.strip()]
543
+
544
+ # Update the notebook cells
545
+ notebook.cells = non_empty_cells
546
+
547
+ # Save the updated notebook
548
+ save_path = output_path if output_path else notebook_path
549
+ with open(save_path, "w") as nb_file:
550
+ nbformat.write(notebook, nb_file)
551
+
552
+ print(f"Empty cells removed. Updated notebook saved at: {save_path}")
553
+
554
+ except Exception as e:
555
+ print(f"An error occurred: {e}")
556
+
557
+ def add_submission_cells(self, notebook_path: str, output_path: str) -> None:
558
+ """
559
+ Adds submission cells to the end of a Jupyter notebook.
560
+
561
+ Args:
562
+ notebook_path (str): Path to the input notebook.
563
+ output_path (str): Path to save the modified notebook.
564
+ """
565
+ # Load the notebook
566
+ with open(notebook_path, "r", encoding="utf-8") as f:
567
+ notebook = nbformat.read(f, as_version=4)
568
+
569
+ # Define the Markdown cell
570
+ markdown_cell = nbformat.v4.new_markdown_cell(
571
+ "## Submitting Assignment\n\n"
572
+ "Please run the following block of code using `shift + enter` to submit your assignment, "
573
+ "you should see your score."
574
+ )
575
+
576
+ if self.require_key:
577
+ # Add an additional line for validate_token()
578
+ validate_token_line = "from pykubegrader.submit.submit_assignment import validate_token\nvalidate_token()\n"
579
+
580
+ # Define the Code cell
581
+ code_cell = nbformat.v4.new_code_cell(
582
+ f"{validate_token_line}\n\n" # Add the validate_token() line
583
+ "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
584
+ f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
585
+ )
586
+ else:
587
+ # Define the Code cell without validate_token()
588
+ code_cell = nbformat.v4.new_code_cell(
589
+ "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
590
+ f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
591
+ )
592
+
593
+ # Make the code cell non-editable and non-deletable
594
+ code_cell.metadata = {"editable": False, "deletable": False}
595
+ code_cell.metadata["tags"] = ["skip-execution"]
596
+
597
+ # Add the cells to the notebook
598
+ notebook.cells.append(markdown_cell)
599
+ notebook.cells.append(code_cell)
600
+
601
+ # Save the modified notebook
602
+ with open(output_path, "w", encoding="utf-8") as f:
603
+ nbformat.write(notebook, f)
604
+
444
605
  def free_response_parser(
445
606
  self, temp_notebook_path, notebook_subfolder, notebook_name
446
607
  ):
@@ -471,7 +632,9 @@ class NotebookProcessor:
471
632
  shutil.copy("./keys/.server_public_key.bin", server_public_key)
472
633
 
473
634
  out = FastAPINotebookBuilder(
474
- notebook_path=temp_notebook_path, assignment_tag=self.assignment_tag
635
+ notebook_path=temp_notebook_path,
636
+ assignment_tag=self.assignment_tag,
637
+ require_key=self.require_key,
475
638
  )
476
639
 
477
640
  debug_notebook = os.path.join(
@@ -496,7 +659,10 @@ class NotebookProcessor:
496
659
  )
497
660
 
498
661
  NotebookProcessor.add_initialization_code(
499
- student_notebook, self.week, self.assignment_type
662
+ student_notebook,
663
+ self.week,
664
+ self.assignment_type,
665
+ require_key=self.require_key,
500
666
  )
501
667
 
502
668
  NotebookProcessor.replace_temp_in_notebook(
@@ -523,7 +689,10 @@ class NotebookProcessor:
523
689
  return student_notebook, out.total_points
524
690
  else:
525
691
  NotebookProcessor.add_initialization_code(
526
- temp_notebook_path, self.week, self.assignment_type
692
+ temp_notebook_path,
693
+ self.week,
694
+ self.assignment_type,
695
+ require_key=self.require_key,
527
696
  )
528
697
  NotebookProcessor.replace_temp_no_otter(
529
698
  temp_notebook_path, temp_notebook_path
@@ -555,11 +724,19 @@ class NotebookProcessor:
555
724
  nbformat.write(notebook, f)
556
725
 
557
726
  @staticmethod
558
- def add_initialization_code(notebook_path, week, assignment_type):
727
+ def add_initialization_code(
728
+ notebook_path, week, assignment_type, require_key=False
729
+ ):
559
730
  # finds the first code cell
560
731
  index, cell = find_first_code_cell(notebook_path)
561
732
  cell = cell["source"]
562
- import_text = "from pykubegrader.initialize import initialize_assignment\n"
733
+ import_text = "# You must make sure to run all cells in sequence using shift + enter or you might encounter errors\n"
734
+ import_text += "from pykubegrader.initialize import initialize_assignment\n"
735
+ if require_key:
736
+ import_text += (
737
+ "from pykubegrader.token.validate_token import validate_token\n"
738
+ )
739
+ import_text += "validate_token('type the key provided by your TA here')\n"
563
740
  import_text += f'\nresponses = initialize_assignment("{os.path.splitext(os.path.basename(notebook_path))[0]}", "{week}", "{assignment_type}" )\n'
564
741
  cell = f"{import_text}\n" + cell
565
742
  replace_cell_source(notebook_path, index, cell)
@@ -675,7 +852,7 @@ class NotebookProcessor:
675
852
  return solution_path, question_path
676
853
  else:
677
854
  return None, None
678
-
855
+
679
856
  @staticmethod
680
857
  def replace_temp_no_otter(input_file, output_file):
681
858
  # Load the notebook
@@ -685,8 +862,8 @@ class NotebookProcessor:
685
862
  # Iterate through the cells and modify `cell.source`
686
863
  for cell in notebook.cells:
687
864
  if cell.cell_type == "code": # Only process code cells
688
- if 'responses = initialize_assignment(' in cell.source:
689
- cell.source = cell.source.replace('_temp', '')
865
+ if "responses = initialize_assignment(" in cell.source:
866
+ cell.source = cell.source.replace("_temp", "")
690
867
 
691
868
  # Save the modified notebook
692
869
  with open(output_file, "w", encoding="utf-8") as f:
@@ -715,7 +892,6 @@ class NotebookProcessor:
715
892
  cell["source"] = [
716
893
  line.replace("_temp.ipynb", ".ipynb") for line in cell["source"]
717
894
  ]
718
-
719
895
 
720
896
  # Write the updated notebook to the output file
721
897
  with open(output_file, "w", encoding="utf-8") as f:
@@ -1400,6 +1576,7 @@ def extract_MCQ(ipynb_file):
1400
1576
  print("Invalid JSON in notebook file.")
1401
1577
  return []
1402
1578
 
1579
+
1403
1580
  def check_for_heading(notebook_path, search_strings):
1404
1581
  """
1405
1582
  Checks if a Jupyter notebook contains a heading cell whose source matches any of the given strings.
@@ -1439,8 +1616,6 @@ def clean_notebook(notebook_path):
1439
1616
  cell.metadata["editable"] = cell.metadata.get("editable", False)
1440
1617
  cell.metadata["deletable"] = cell.metadata.get("deletable", False)
1441
1618
  if cell.cell_type == "code":
1442
- if "grader.check(" in cell.source:
1443
- continue
1444
1619
  cell.metadata["tags"] = cell.metadata.get("tags", [])
1445
1620
  if "skip-execution" not in cell.metadata["tags"]:
1446
1621
  cell.metadata["tags"].append("skip-execution")
@@ -1606,7 +1781,9 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
1606
1781
  )
1607
1782
  f.write(" def __init__(self):\n")
1608
1783
  f.write(" super().__init__(\n")
1609
- f.write(f' title=f"{q_value['question_text']}",\n')
1784
+ f.write(
1785
+ f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
1786
+ )
1610
1787
  f.write(" style=MCQ,\n")
1611
1788
  f.write(
1612
1789
  f" question_number={q_value['question number']},\n"
@@ -1677,7 +1854,9 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
1677
1854
  )
1678
1855
  f.write(" def __init__(self):\n")
1679
1856
  f.write(" super().__init__(\n")
1680
- f.write(f" title=f'{q_value['question_text']}',\n")
1857
+ f.write(
1858
+ f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
1859
+ )
1681
1860
  f.write(" style=MultiSelect,\n")
1682
1861
  f.write(
1683
1862
  f" question_number={q_value['question number']},\n"
@@ -1754,7 +1933,9 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
1754
1933
  )
1755
1934
  f.write(" def __init__(self):\n")
1756
1935
  f.write(" super().__init__(\n")
1757
- f.write(f" title=f'{q_value['question_text']}',\n")
1936
+ f.write(
1937
+ f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
1938
+ )
1758
1939
  f.write(" style=TFStyle,\n")
1759
1940
  f.write(
1760
1941
  f" question_number={q_value['question number']},\n"
@@ -1845,7 +2026,8 @@ def replace_cell_source(notebook_path, cell_index, new_source):
1845
2026
  # Save the notebook
1846
2027
  with open(notebook_path, "w", encoding="utf-8") as f:
1847
2028
  nbformat.write(notebook, f)
1848
-
2029
+
2030
+
1849
2031
  def update_initialize_assignment(
1850
2032
  notebook_path: str,
1851
2033
  assignment_points: Optional[float] = None,
@@ -1893,11 +2075,11 @@ def update_initialize_assignment(
1893
2075
  existing_args = match.group(1).strip()
1894
2076
  # Replace with the updated line
1895
2077
  if additional_variables_str:
2078
+ updated_line = f"responses = initialize_assignment({existing_args}, {additional_variables_str})\n"
2079
+ else:
1896
2080
  updated_line = (
1897
- f"responses = initialize_assignment({existing_args}, {additional_variables_str})\n"
2081
+ f"responses = initialize_assignment({existing_args})\n"
1898
2082
  )
1899
- else:
1900
- updated_line = f"responses = initialize_assignment({existing_args})\n"
1901
2083
  source_code[i] = updated_line
1902
2084
  updated = True
1903
2085
 
@@ -1925,9 +2107,18 @@ def main():
1925
2107
  default="Reading-Week-X",
1926
2108
  )
1927
2109
 
2110
+ parser.add_argument(
2111
+ "--require-key",
2112
+ type=bool,
2113
+ help="Require a key to be generated for the assignment",
2114
+ default=False,
2115
+ )
2116
+
1928
2117
  args = parser.parse_args()
1929
2118
  processor = NotebookProcessor(
1930
- root_folder=args.root_folder, assignment_tag=args.assignment_tag
2119
+ root_folder=args.root_folder,
2120
+ assignment_tag=args.assignment_tag,
2121
+ require_key=args.require_key,
1931
2122
  )
1932
2123
  processor.process_notebooks()
1933
2124
 
@@ -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:
@@ -1,9 +1,10 @@
1
1
  import os
2
2
  import httpx
3
3
  import asyncio
4
-
5
4
  import nest_asyncio
5
+ import base64
6
6
 
7
+ # Apply nest_asyncio for environments like Jupyter
7
8
  nest_asyncio.apply()
8
9
 
9
10
 
@@ -21,7 +22,7 @@ def get_credentials():
21
22
 
22
23
 
23
24
  async def call_score_assignment(
24
- assignment_title: str, file_path: str = ".output_reduced.log"
25
+ assignment_title: str, notebook_title: str, file_path: str = ".output_reduced.log"
25
26
  ) -> dict:
26
27
  """
27
28
  Submit an assignment to the scoring endpoint.
@@ -37,10 +38,17 @@ async def call_score_assignment(
37
38
  base_url = os.getenv("DB_URL")
38
39
  if not base_url:
39
40
  raise ValueError("Environment variable 'DB_URL' is not set.")
40
- url = f"{base_url}score-assignment"
41
+ url = f"{base_url}score-assignment?assignment_title={assignment_title}&notebook_title={notebook_title}"
41
42
 
42
43
  # Get credentials
43
44
  credentials = get_credentials()
45
+ username = credentials["username"]
46
+ password = credentials["password"]
47
+
48
+ # Encode credentials for Basic Authentication
49
+ auth_header = (
50
+ f"Basic {base64.b64encode(f'{username}:{password}'.encode()).decode()}"
51
+ )
44
52
 
45
53
  # Send the POST request
46
54
  async with httpx.AsyncClient() as client:
@@ -48,8 +56,8 @@ async def call_score_assignment(
48
56
  with open(file_path, "rb") as file:
49
57
  response = await client.post(
50
58
  url,
51
- data={"cred": credentials, "assignment_title": assignment_title},
52
- files={"log_file": file},
59
+ headers={"Authorization": auth_header}, # Add Authorization header
60
+ files={"log_file": file}, # Upload log file
53
61
  )
54
62
 
55
63
  # Handle the response
@@ -64,9 +72,10 @@ async def call_score_assignment(
64
72
  raise RuntimeError(f"An unexpected error occurred: {e}")
65
73
 
66
74
 
67
- # Importable function
68
75
  def submit_assignment(
69
- assignment_title: str, file_path: str = ".output_reduced.log"
76
+ assignment_title: str,
77
+ notebook_title: str,
78
+ file_path: str = ".output_reduced.log",
70
79
  ) -> None:
71
80
  """
72
81
  Synchronous wrapper for the `call_score_assignment` function.
@@ -75,10 +84,20 @@ def submit_assignment(
75
84
  assignment_title (str): Title of the assignment.
76
85
  file_path (str): Path to the log file to upload.
77
86
  """
78
- response = asyncio.run(call_score_assignment(assignment_title, file_path))
87
+ # Get the current event loop or create one
88
+ try:
89
+ loop = asyncio.get_event_loop()
90
+ except RuntimeError:
91
+ loop = asyncio.new_event_loop()
92
+ asyncio.set_event_loop(loop)
93
+
94
+ # Run the async function in the event loop
95
+ response = loop.run_until_complete(
96
+ call_score_assignment(assignment_title, notebook_title, file_path)
97
+ )
79
98
  print("Server Response:", response.get("message", "No message in response"))
80
99
 
81
100
 
82
101
  # Example usage (remove this section if only the function needs to be importable):
83
102
  if __name__ == "__main__":
84
- submit_assignment("Week 1 Assignment", "path/to/your/log_file.txt")
103
+ submit_assignment("week1-readings", "path/to/your/log_file.txt")
@@ -0,0 +1,47 @@
1
+ import requests
2
+ import os
3
+ import json
4
+
5
+
6
+ def build_token_payload(token: str, duration: int) -> dict:
7
+
8
+ if os.getenv("JUPYTERHUB_USER", None) is None:
9
+ raise ValueError("JupyterHub user not found")
10
+
11
+ # Return the extracted details as a dictionary
12
+ return {
13
+ "value": token,
14
+ "requester": os.getenv("JUPYTERHUB_USER", None),
15
+ "duration": duration,
16
+ }
17
+
18
+
19
+ # Need to do for add token as well
20
+ def add_token(token, duration=20):
21
+ """
22
+ Sends a POST request to add a notebook.
23
+ """
24
+ # Define the URL
25
+ url = "https://engr-131-api.eastus.cloudapp.azure.com/tokens"
26
+
27
+ # Build the payload
28
+ payload = build_token_payload(token=token, duration=duration)
29
+
30
+ # Define HTTP Basic Authentication
31
+ auth = ("user", "password")
32
+
33
+ # Define headers
34
+ headers = {"Content-Type": "application/json"}
35
+
36
+ # Serialize the payload with the custom JSON encoder
37
+ serialized_payload = json.dumps(payload)
38
+
39
+ # Send the POST request
40
+ response = requests.post(url, data=serialized_payload, headers=headers, auth=auth)
41
+
42
+ # Print the response
43
+ print(f"Status Code: {response.status_code}")
44
+ try:
45
+ print(f"Response: {response.json()}")
46
+ except ValueError:
47
+ print(f"Response: {response.text}")
@@ -0,0 +1,138 @@
1
+ import os
2
+ import base64
3
+ import httpx
4
+ import asyncio
5
+ import nest_asyncio
6
+
7
+ # Apply nest_asyncio for environments like Jupyter
8
+ nest_asyncio.apply()
9
+
10
+
11
+ class TokenValidationError(Exception):
12
+ """
13
+ Custom exception raised when the token validation fails.
14
+ """
15
+
16
+ def __init__(self, message=None):
17
+ """
18
+ Initialize the exception with an optional message.
19
+
20
+ Args:
21
+ message (str, optional): The error message to display. Defaults to None.
22
+ """
23
+ super().__init__(message)
24
+
25
+
26
+ def get_credentials():
27
+ """
28
+ Fetch the username and password from environment variables.
29
+
30
+ Returns:
31
+ dict: A dictionary containing 'username' and 'password'.
32
+ """
33
+ username = os.getenv("user_name_student")
34
+ password = os.getenv("keys_student")
35
+ if not username or not password:
36
+ raise ValueError(
37
+ "Environment variables 'user_name_student' or 'keys_student' are not set."
38
+ )
39
+ return {"username": username, "password": password}
40
+
41
+
42
+ async def async_validate_token(token: str = None) -> None:
43
+ """
44
+ Asynchronously validate a token by making a GET request to the validation endpoint.
45
+
46
+ Args:
47
+ token (str): The token to validate.
48
+
49
+ Raises:
50
+ TokenValidationError: If the token is invalid or if there is an error in the validation process.
51
+
52
+ Returns:
53
+ None: If the token is valid, the function will pass silently.
54
+ """
55
+
56
+ if token is not None:
57
+ os.environ["TOKEN"] = token
58
+
59
+ if token is None:
60
+ token = os.getenv("TOKEN", None)
61
+
62
+ if token is None:
63
+ raise TokenValidationError("No token provided")
64
+
65
+ # Fetch the endpoint URL
66
+ base_url = os.getenv("DB_URL")
67
+ if not base_url:
68
+ raise ValueError("Environment variable 'DB_URL' is not set.")
69
+ endpoint = f"{base_url}validate-token/{token}"
70
+
71
+ # Get credentials
72
+ credentials = get_credentials()
73
+ username = credentials["username"]
74
+ password = credentials["password"]
75
+
76
+ # Encode credentials for Basic Authentication
77
+ auth_header = (
78
+ f"Basic {base64.b64encode(f'{username}:{password}'.encode()).decode()}"
79
+ )
80
+
81
+ # Make the GET request
82
+ async with httpx.AsyncClient() as client:
83
+ try:
84
+ response = await client.get(
85
+ endpoint, headers={"Authorization": auth_header}, timeout=10
86
+ )
87
+
88
+ if response.status_code == 200:
89
+ # If the response is 200, the token is valid
90
+ return # Pass silently
91
+ elif response.status_code == 404:
92
+ # If the response is 404, the token is invalid
93
+ detail = response.json().get("detail", "Token not found")
94
+ raise TokenValidationError(detail)
95
+ else:
96
+ # Handle unexpected status codes
97
+ raise TokenValidationError(
98
+ f"Unexpected response code: {response.status_code}"
99
+ )
100
+ except httpx.RequestError as e:
101
+ raise TokenValidationError(f"Request failed: {e}")
102
+ except Exception as e:
103
+ raise TokenValidationError(f"An unexpected error occurred: {e}")
104
+
105
+
106
+ def validate_token(token: str = None) -> None:
107
+ """
108
+ Synchronous wrapper for the `async_validate_token` function.
109
+
110
+ Args:
111
+ token (str): The token to validate.
112
+
113
+ Raises:
114
+ TokenValidationError: If the token is invalid or if there is an error in the validation process.
115
+
116
+ Returns:
117
+ None: If the token is valid, the function will pass silently.
118
+ """
119
+
120
+ # Get the current event loop or create one
121
+ try:
122
+ loop = asyncio.get_event_loop()
123
+ except RuntimeError:
124
+ loop = asyncio.new_event_loop()
125
+ asyncio.set_event_loop(loop)
126
+
127
+ # Run the async function in the event loop
128
+ loop.run_until_complete(async_validate_token(token))
129
+
130
+
131
+ # Example usage
132
+ if __name__ == "__main__":
133
+ token = "test"
134
+ try:
135
+ validate_token(token)
136
+ print("Token is valid.")
137
+ except TokenValidationError as e:
138
+ print(f"Token validation failed: {e}")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes