PyKubeGrader 0.3.5__py3-none-any.whl → 0.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyKubeGrader
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -1,12 +1,12 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
2
  pykubegrader/grading_tester.ipynb,sha256=wwT9jyhpR6GGM8r4todaGfrsUxS6JxM0qIqMcDYKM7w,18839
3
3
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
4
- pykubegrader/telemetry.py,sha256=ooLK-dY_hJQ7t4r83hWyO8wx6F_7TfWJS7tCp_nH7r8,13049
4
+ pykubegrader/telemetry.py,sha256=vZK9p3XqnqacwtiVyZgjI2mcIr5ZcxRRwW5sAZsrJkE,16631
5
5
  pykubegrader/utils.py,sha256=jlJklKvRhY3O7Hz2aaU1m0y3p_n9eMAXNnAF7LUEaPY,1275
6
6
  pykubegrader/validate.py,sha256=OKnItGyd-L8QPKcsE0KRuwBI_IxKiJzMLJKZiA2j3II,11184
7
7
  pykubegrader/build/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
- pykubegrader/build/api_notebook_builder.py,sha256=320aFB1-6KLQt0OMmKD0FJtE17JKZaK5neA0mTD-Y8M,22960
9
- pykubegrader/build/build_folder.py,sha256=Asc-VdhXgxQfOfFIWJShhXrF2EITJOIZQ5Dz_2y-P2I,85358
8
+ pykubegrader/build/api_notebook_builder.py,sha256=EZG4Ow4YATzOWPPNLkdQEdWt7hkpbaI5ZD1Bf2KEWeY,25622
9
+ pykubegrader/build/build_folder.py,sha256=HDccW597shBhAd-jxvMUPpsiNj2S4DlKRGMDH_6U9nQ,87590
10
10
  pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
11
11
  pykubegrader/build/collate.py,sha256=cVvF7tf2U3iiH4R_dbghTcieedIx5w3Fyw9L_llInM8,6754
12
12
  pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
@@ -16,9 +16,10 @@ pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl
16
16
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
17
17
  pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
18
18
  pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
19
- pykubegrader/submit/submit_assignment.py,sha256=cqVu7US8GVaCdJdaU2yjawlVBtAKP5XJc4oAvX5FeRU,2575
20
- pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
21
- pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
19
+ pykubegrader/submit/submit_assignment.py,sha256=Cx9hc2A--ts95dFLvn7Fw--ekRwPXqHKKVChnGFgw_Y,2670
20
+ pykubegrader/tokens/token_panel.py,sha256=NNA5ZV3Q9jB_lz2aSwMyViXV0ESu6V_7T92Qji7UpSQ,1377
21
+ pykubegrader/tokens/tokens.py,sha256=qcYMFgNPimbfeS7lXOtbgquGgeJCgOGx5hvXewIs0oQ,1474
22
+ pykubegrader/tokens/validate_token.py,sha256=kvHX0NJBm21xzb2p67j7vq1La6J1XbmobEJQ3fTMdZA,3289
22
23
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
24
  pykubegrader/widgets/multiple_choice.py,sha256=ag6W-HN7isHkIUmB4BxtK8T1JhuV3FBLUBAhcV6rN80,2729
24
25
  pykubegrader/widgets/question_processor.py,sha256=fFH2ffMPYAJHsDn1RweEBnibfoZlSvTANUxYT3EPb5w,1375
@@ -32,9 +33,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
32
33
  pykubegrader/widgets_base/multi_select.py,sha256=KtfAP0PyEbcjWlKNpI5_5-PLMtcUbbNX0Es_-w-H34Q,4226
33
34
  pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
34
35
  pykubegrader/widgets_base/select.py,sha256=uMncmVIqjvJkffMQY1L_PokrFCidK1PeVITX0i70fho,2750
35
- PyKubeGrader-0.3.5.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
36
- PyKubeGrader-0.3.5.dist-info/METADATA,sha256=a5vrrGdf4XTTSfKPAuQ-2HaHISFczwu6uSy6KkrrQv4,2729
37
- PyKubeGrader-0.3.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
38
- PyKubeGrader-0.3.5.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
39
- PyKubeGrader-0.3.5.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
40
- PyKubeGrader-0.3.5.dist-info/RECORD,,
36
+ PyKubeGrader-0.3.7.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
37
+ PyKubeGrader-0.3.7.dist-info/METADATA,sha256=Ht8G9VvFfGBa9YD9PoBSbd_Cee6tuGZ2RTBjFN0Z5pk,2729
38
+ PyKubeGrader-0.3.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
+ PyKubeGrader-0.3.7.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
40
+ PyKubeGrader-0.3.7.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
41
+ PyKubeGrader-0.3.7.dist-info/RECORD,,
@@ -17,12 +17,15 @@ class FastAPINotebookBuilder:
17
17
  temp_notebook: Optional[str] = None
18
18
  assignment_tag: Optional[str] = ""
19
19
  require_key: Optional[bool] = False
20
+ verbose: Optional[bool] = False
20
21
 
21
22
  def __post_init__(self) -> None:
22
23
  self.root_path, self.filename = FastAPINotebookBuilder.get_filename_and_root(
23
24
  self.notebook_path
24
25
  )
25
26
  self.total_points = 0
27
+
28
+ self.max_question_points = {}
26
29
  self.run()
27
30
 
28
31
  def run(self) -> None:
@@ -38,6 +41,9 @@ class FastAPINotebookBuilder:
38
41
  self.assertion_tests_dict = self.question_dict()
39
42
  self.add_api_code()
40
43
 
44
+ # add the point total to the end of the notebook
45
+ self.add_total_points_to_notebook()
46
+
41
47
  @staticmethod
42
48
  def conceal_tests(cell_source):
43
49
  """
@@ -76,28 +82,33 @@ class FastAPINotebookBuilder:
76
82
 
77
83
  return concealed_lines
78
84
 
79
- def add_api_code(self):
80
85
  def add_api_code(self) -> None:
81
86
  self.compute_max_points_free_response()
87
+ for i, question in enumerate(self.max_question_points.keys()):
88
+ index, source = self.find_question_description(question)
89
+ try:
90
+ modified_source = FastAPINotebookBuilder.add_text_after_double_hash(source, f"Question {i+1} (Points: {self.max_question_points[question]}):")
91
+ self.replace_cell_source(index, modified_source)
92
+ except:
93
+ pass
82
94
 
83
95
  for i, (cell_index, cell_dict) in enumerate(self.assertion_tests_dict.items()):
84
- print(
85
- f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
86
- )
96
+ if self.verbose:
97
+ print(
98
+ f"Processing cell {cell_index + 1}, {i} of {len(self.assertion_tests_dict)}"
99
+ )
87
100
 
88
101
  cell = self.get_cell(cell_index)
89
102
  cell_source = FastAPINotebookBuilder.add_import_statements_to_tests(
90
- cell["source"]
103
+ cell["source"], require_key=self.require_key,
91
104
  )
92
-
105
+
93
106
  cell_source = FastAPINotebookBuilder.conceal_tests(cell_source)
94
107
 
95
108
  last_import_line_ind = FastAPINotebookBuilder.find_last_import_line(
96
109
  cell_source
97
110
  )
98
111
 
99
- # header, body = FastAPINotebookBuilder.split_list_at_marker(cell_source)
100
-
101
112
  updated_cell_source = []
102
113
  updated_cell_source.extend(cell_source[: last_import_line_ind + 1])
103
114
  if cell_dict["is_first"]:
@@ -135,18 +146,65 @@ class FastAPINotebookBuilder:
135
146
  FastAPINotebookBuilder.construct_update_responses(cell_dict)
136
147
  )
137
148
 
138
- self.replace_cell_source(cell_index, updated_cell_source)
149
+ self.replace_cell_source(cell_index, updated_cell_source)
150
+
151
+ def find_question_description(self, search_string):
152
+ with open(self.temp_notebook, 'r', encoding='utf-8') as f:
153
+ nb_data = json.load(f)
154
+
155
+ found_raw = False
156
+
157
+ for idx, cell in enumerate(nb_data.get("cells", [])):
158
+ if cell["cell_type"] == "raw" and any("# BEGIN QUESTION" in line for line in cell.get("source", [])) and any(search_string in line for line in cell.get("source", [])):
159
+ found_raw = True
160
+ elif found_raw and cell["cell_type"] == "markdown":
161
+ return idx, cell.get("source", []) # Return the index of the first matching markdown cell
162
+
163
+ return None, None # Return None if no such markdown cell is found
164
+
165
+ def add_total_points_to_notebook(self) -> None:
166
+ self.max_question_points.keys()
167
+
168
+ def get_max_question_points(self, cell_dict) -> float:
169
+ return sum(
170
+ cell["points"]
171
+ for cell in self.assertion_tests_dict.values()
172
+ if cell["question"] == cell_dict["question"]
173
+ )
174
+
175
+ @staticmethod
176
+ def add_text_after_double_hash(markdown_source, insert_text):
177
+ """
178
+ Adds insert_text immediately after the first '##' in the first line that starts with '##'.
179
+
180
+ Args:
181
+ - markdown_source (list of str): The list of lines in the markdown cell.
182
+ - insert_text (str): The text to be inserted.
183
+
184
+ Returns:
185
+ - list of str: The modified markdown cell content.
186
+ """
187
+ modified_source = []
188
+ inserted = False
189
+
190
+ for line in markdown_source:
191
+ if not inserted and line.startswith("## "):
192
+ modified_source.append(f"## {insert_text} {line[3:]}") # Insert text after '##'
193
+ inserted = True # Ensure it only happens once
194
+ else:
195
+ modified_source.append(line)
196
+
197
+ return modified_source
139
198
 
140
199
  def compute_max_points_free_response(self) -> None:
141
200
  for cell_dict in self.assertion_tests_dict.values():
142
201
  # gets the question name from the first cell to not double count
143
202
  if cell_dict["is_first"]:
144
203
  # get the max points for the question
145
- max_question_points = sum(
146
- cell["points"]
147
- for cell in self.assertion_tests_dict.values()
148
- if cell["question"] == cell_dict["question"]
149
- )
204
+ max_question_points = self.get_max_question_points(cell_dict)
205
+
206
+ # store the max points for the question
207
+ self.max_question_points[f"{cell_dict["question"]}"] = max_question_points
150
208
 
151
209
  self.total_points += max_question_points
152
210
 
@@ -272,7 +330,7 @@ class FastAPINotebookBuilder:
272
330
  return original_list[:index] + insert_list + original_list[index:]
273
331
 
274
332
  @staticmethod
275
- def add_import_statements_to_tests(cell_source: list[str]) -> list[str]:
333
+ def add_import_statements_to_tests(cell_source: list[str], require_key:bool = False) -> list[str]:
276
334
  """
277
335
  Adds the necessary import statements to the first cell of the notebook.
278
336
  """
@@ -293,6 +351,11 @@ class FastAPINotebookBuilder:
293
351
  "import base64\n",
294
352
  ]
295
353
 
354
+ if require_key:
355
+ imports.append(
356
+ "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
357
+ )
358
+
296
359
  for i, line in enumerate(cell_source):
297
360
  if end_test_config_line in line:
298
361
  # Insert the imports immediately after the current line
@@ -77,15 +77,21 @@ class NotebookProcessor:
77
77
  data = yaml.safe_load(file)
78
78
  # Extract assignment details
79
79
  assignment = data.get("assignment", {})
80
- week_num = assignment.get("week")
80
+ self.week_num = assignment.get("week")
81
81
  self.assignment_type = assignment.get("assignment_type")
82
82
  self.bonus_points = assignment.get("bonus_points", 0)
83
+ self.require_key = assignment.get("require_key", False)
84
+ self.assignment_tag = assignment.get(
85
+ "assignment_tag",
86
+ f"week{assignment.get("week")}-{self.assignment_type}",
87
+ )
83
88
  else:
84
89
  self.assignment_type = self.assignment_tag.split("-")[0].lower()
85
- week_num = self.assignment_tag.split("-")[-1]
90
+ self.week_num = self.assignment_tag.split("-")[-1]
91
+ self.assignment_tag = f"week{self.week_num}-{self.assignment_type}"
86
92
 
87
- self.week_num = week_num
88
- self.week = f"week_{week_num}"
93
+ # self.week_num = week_num
94
+ self.week = f"week_{self.week_num}"
89
95
 
90
96
  # Define the folder to store solutions and ensure it exists
91
97
  self.solutions_folder = os.path.join(self.root_folder, "_solutions")
@@ -179,12 +185,12 @@ class NotebookProcessor:
179
185
 
180
186
  def update_initialize_function(self):
181
187
  for key, value in self.total_point_log.items():
182
- assignment_tag = f"week{self.week_num}-{self.assignment_type}"
188
+ # assignment_tag = f"week{self.week_num}-{self.assignment_type}"
183
189
 
184
190
  update_initialize_assignment(
185
191
  notebook_path=os.path.join(self.root_folder, key + ".ipynb"),
186
192
  assignment_points=value,
187
- assignment_tag=assignment_tag,
193
+ assignment_tag=self.assignment_tag,
188
194
  )
189
195
 
190
196
  def build_payload(self, yaml_content):
@@ -572,23 +578,23 @@ class NotebookProcessor:
572
578
 
573
579
  if self.require_key:
574
580
  # Add an additional line for validate_token()
575
- validate_token_line = "from pykubegrader.tokens.validate_token import validate_token\nvalidate_token()\n"
581
+ validate_token_line =f"from pykubegrader.tokens.validate_token import validate_token\nvalidate_token(assignment = '{self.assignment_tag}')\n"
576
582
 
577
583
  # Define the Code cell
578
584
  code_cell = nbformat.v4.new_code_cell(
579
585
  f"{validate_token_line}\n\n" # Add the validate_token() line
580
586
  "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
581
- f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
587
+ f'submit_assignment("{self.assignment_tag}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
582
588
  )
583
589
  else:
584
590
  # Define the Code cell without validate_token()
585
591
  code_cell = nbformat.v4.new_code_cell(
586
592
  "from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
587
- f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
593
+ f'submit_assignment("{self.assignment_tag}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
588
594
  )
589
595
 
590
596
  # Make the code cell non-editable and non-deletable
591
- code_cell.metadata = {"editable": False, "deletable": False}
597
+ code_cell.metadata = {"editable": True, "deletable": False}
592
598
  code_cell.metadata["tags"] = ["skip-execution"]
593
599
 
594
600
  # Add the cells to the notebook
@@ -675,6 +681,7 @@ class NotebookProcessor:
675
681
  self.week,
676
682
  self.assignment_type,
677
683
  require_key=self.require_key,
684
+ assignment_tag = self.assignment_tag,
678
685
  )
679
686
 
680
687
  NotebookProcessor.replace_temp_in_notebook(
@@ -705,6 +712,7 @@ class NotebookProcessor:
705
712
  self.week,
706
713
  self.assignment_type,
707
714
  require_key=self.require_key,
715
+ assignment_tag = self.assignment_tag
708
716
  )
709
717
  NotebookProcessor.replace_temp_no_otter(
710
718
  temp_notebook_path, temp_notebook_path
@@ -736,7 +744,7 @@ class NotebookProcessor:
736
744
  nbformat.write(notebook, f)
737
745
 
738
746
  @staticmethod
739
- def add_validate_token_cell(notebook_path: str, require_key: bool) -> None:
747
+ def add_validate_token_cell(notebook_path: str, require_key: bool, **kwargs) -> None:
740
748
  """
741
749
  Adds a new code cell at the top of a Jupyter notebook if require_key is True.
742
750
 
@@ -750,16 +758,24 @@ class NotebookProcessor:
750
758
  if not require_key:
751
759
  print("require_key is False. No changes made to the notebook.")
752
760
  return
761
+
762
+ NotebookProcessor.add_validate_block(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
753
763
 
754
764
  # Load the notebook
755
765
  with open(notebook_path, "r", encoding="utf-8") as f:
756
766
  notebook = nbformat.read(f, as_version=4)
757
767
 
758
768
  # Create the new code cell
759
- new_cell = nbformat.v4.new_code_cell(
769
+ if kwargs.get("assignment_tag", None):
770
+ new_cell = nbformat.v4.new_code_cell(
771
+ "from pykubegrader.tokens.validate_token import validate_token\n"
772
+ f"validate_token('type the key provided by your instructor here', assignment = '{kwargs.get('assignment_tag')}')\n"
773
+ )
774
+ else:
775
+ new_cell = nbformat.v4.new_code_cell(
760
776
  "from pykubegrader.tokens.validate_token import validate_token\n"
761
777
  "validate_token('type the key provided by your instructor here')\n"
762
- )
778
+ )
763
779
 
764
780
  # Add the new cell to the top of the notebook
765
781
  notebook.cells.insert(0, new_cell)
@@ -768,9 +784,42 @@ class NotebookProcessor:
768
784
  with open(notebook_path, "w", encoding="utf-8") as f:
769
785
  nbformat.write(notebook, f)
770
786
 
787
+ @staticmethod
788
+ def add_validate_block(notebook_path: str, require_key: bool, **kwargs) -> None:
789
+ """
790
+ Modifies the first code cell of a Jupyter notebook to add the validate_token call if require_key is True.
791
+
792
+ Args:
793
+ notebook_path (str): The path to the notebook file to modify.
794
+ require_key (bool): Whether to add the validate_token cell.
795
+
796
+ Returns:
797
+ None
798
+ """
799
+ if not require_key:
800
+ return
801
+
802
+ # Load the notebook
803
+ with open(notebook_path, "r", encoding="utf-8") as f:
804
+ notebook = nbformat.read(f, as_version=4)
805
+
806
+ # Prepare the validation code
807
+ validation_code = "validate_token()\n"
808
+
809
+ # Modify the first cell if it's a code cell, otherwise insert a new one
810
+ if notebook.cells and notebook.cells[0].cell_type == "code":
811
+ notebook.cells[0].source = validation_code + "\n" + notebook.cells[0].source
812
+ else:
813
+ new_cell = nbformat.v4.new_code_cell(validation_code)
814
+ notebook.cells.insert(0, new_cell)
815
+
816
+ # Save the modified notebook
817
+ with open(notebook_path, "w", encoding="utf-8") as f:
818
+ nbformat.write(notebook, f)
819
+
771
820
  @staticmethod
772
821
  def add_initialization_code(
773
- notebook_path, week, assignment_type, require_key=False
822
+ notebook_path, week, assignment_type, require_key=False, **kwargs,
774
823
  ):
775
824
  # finds the first code cell
776
825
  index, cell = find_first_code_cell(notebook_path)
@@ -782,7 +831,7 @@ class NotebookProcessor:
782
831
  replace_cell_source(notebook_path, index, cell)
783
832
 
784
833
  if require_key:
785
- NotebookProcessor.add_validate_token_cell(notebook_path, require_key)
834
+ NotebookProcessor.add_validate_token_cell(notebook_path, require_key, assignment_tag = kwargs.get("assignment_tag", None))
786
835
 
787
836
  def multiple_choice_parser(self, temp_notebook_path, new_notebook_path):
788
837
  ### Parse the notebook for multiple choice questions
@@ -42,6 +42,9 @@ def call_score_assignment(
42
42
  "assignment_title": assignment_title,
43
43
  "notebook_title": notebook_title,
44
44
  }
45
+
46
+ if os.environ["TOKEN"] is not None:
47
+ params["key_used"] = os.environ["TOKEN"]
45
48
 
46
49
  username, password = get_credentials().values()
47
50
 
pykubegrader/telemetry.py CHANGED
@@ -303,7 +303,7 @@ def get_assignments_submissions():
303
303
  from_env = os.getenv("JUPYTERHUB_USER")
304
304
  if from_hostname != from_env:
305
305
  raise ValueError("Problem with JupyterHub username")
306
-
306
+ print(from_env)
307
307
  params = {"username": from_env}
308
308
  # get submission information
309
309
  res = requests.get(
@@ -319,21 +319,33 @@ def setup_grades_df(assignments):
319
319
 
320
320
  inds = [f"week{i + 1}" for i in range(11)] + ["Running Avg"]
321
321
  restruct_grades = {k: [0 for i in range(len(inds))] for k in assignment_types}
322
- restruct_grades["inds"] = inds
323
- new_weekly_grades = pd.DataFrame(restruct_grades)
322
+ new_weekly_grades = pd.DataFrame(restruct_grades,dtype=float)
323
+ new_weekly_grades["inds"] = inds
324
324
  new_weekly_grades.set_index("inds", inplace=True)
325
325
  return new_weekly_grades
326
326
 
327
327
 
328
+ def skipped_assignment_mask(assignments):
329
+ existing_assignment_mask = setup_grades_df(assignments).astype(bool)
330
+ for assignment in assignments:
331
+ # existing_assignment_mask[assignment["assignment_type"]].iloc[assignment["week_number"]-1] = True
332
+ existing_assignment_mask.loc[f'week{assignment["week_number"]}', assignment["assignment_type"]] = True
333
+ return existing_assignment_mask.astype(bool)
334
+
328
335
  def fill_grades_df(new_weekly_grades, assignments, student_subs):
329
336
  for assignment in assignments:
330
337
  # get the assignment from all submissions
331
- subs = [
332
- sub
333
- for sub in student_subs
334
- if sub["assignment_type"] == assignment["assignment_type"]
335
- and sub["week_number"] == assignment["week_number"]
336
- ]
338
+ subs = [ sub for sub in student_subs if (sub['assignment_type']==assignment['assignment_type']) and (sub['week_number']==assignment['week_number']) ]
339
+ # print(assignment, subs)
340
+ # print(assignment)
341
+ # print(student_subs[:5])
342
+ if assignment["assignment_type"] == "lecture":
343
+ if sum([sub["raw_score"] for sub in subs]) > 0: # TODO: good way to check for completion?
344
+ new_weekly_grades.loc[f"week{assignment['week_number']}", "lecture"] = 1.0
345
+ if assignment["assignment_type"] == "final":
346
+ continue
347
+ if assignment["assignment_type"] == "midterm":
348
+ continue
337
349
  if len(subs) == 0:
338
350
  # print(assignment['title'], 0, assignment['max_score'])
339
351
  continue
@@ -363,22 +375,18 @@ def fill_grades_df(new_weekly_grades, assignments, student_subs):
363
375
  f"week{assignment['week_number']}", assignment["assignment_type"]
364
376
  ] = grade
365
377
 
366
- # Merge different names
367
- new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(
368
- axis=1
369
- )
370
- new_weekly_grades["practicequiz"] = new_weekly_grades[
371
- ["practicequiz", "practice-quiz"]
372
- ].max(axis=1)
373
- new_weekly_grades.drop(
374
- ["attendance", "practice-quiz", "test"],
375
- axis=1,
376
- inplace=True,
377
- errors="ignore",
378
- )
379
-
380
- return new_weekly_grades
378
+ # Merge different names
379
+ new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(axis=1)
380
+ new_weekly_grades["practicequiz"] = new_weekly_grades[["practicequiz", "practice-quiz"]].max(axis=1)
381
+ new_weekly_grades["practicemidterm"] = new_weekly_grades[["practicemidterm", "PracticeMidterm"]].max(axis=1)
382
+ new_weekly_grades.drop(
383
+ ["attendance", "practice-quiz", "test", "PracticeMidterm"],
384
+ axis=1,
385
+ inplace=True,
386
+ errors="ignore",
387
+ )
381
388
 
389
+ return new_weekly_grades
382
390
 
383
391
  def get_current_week(start_date):
384
392
  # Calculate the current week (1-based indexing)
@@ -388,13 +396,36 @@ def get_current_week(start_date):
388
396
  return days_since_start // 7 + 1
389
397
 
390
398
 
399
+ def get_average_weighted_grade(assignments, current_week, new_weekly_grades, weights):
400
+ # Get average until current week
401
+ skip_weeks = skipped_assignment_mask(assignments)
402
+ for col in new_weekly_grades.columns:
403
+ new_weekly_grades.loc["Running Avg", col] = new_weekly_grades.loc[skip_weeks[col]==True, col].mean()
404
+ # for col in new_weekly_grades.columns:
405
+ # skip_weeks = skipped_assignment_mask(assignments)
406
+ # skip_weeks_series = pd.Series(skip_weeks)
407
+ # # new_weekly_grades.iloc[-1,col] = new_weekly_grades.iloc[skip_weeks_series[col],-1].mean()
408
+ # new_weekly_grades
409
+
410
+ # make new dataframe with the midterm, final, and running average
411
+ total = 0
412
+ avg_grades_dict = {}
413
+ for k, v in weights.items():
414
+ grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
415
+ total += grade * v
416
+ avg_grades_dict[k] = grade
417
+ avg_grades_dict['Total'] = total # excluded midterm and final
418
+
419
+ return avg_grades_dict
420
+
421
+
391
422
  # This function currently has many undefined variables and other problems!
392
- def get_my_grades_testing(start_date="2025-01-06"):
423
+ def get_my_grades_testing(start_date="2025-01-06", verbose=True):
393
424
  """takes in json.
394
425
  reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
395
426
  fills in 0 for missing assignments
396
427
  calculate running average of each category"""
397
-
428
+
398
429
  # set up new df format
399
430
  weights = {
400
431
  "homework": 0.15,
@@ -408,23 +439,71 @@ def get_my_grades_testing(start_date="2025-01-06"):
408
439
  }
409
440
 
410
441
  assignments, student_subs = get_assignments_submissions()
411
-
442
+
412
443
  new_grades_df = setup_grades_df(assignments)
413
444
 
414
445
  new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
415
446
 
416
447
  current_week = get_current_week(start_date)
448
+
449
+ avg_grades_dict = get_average_weighted_grade(assignments, current_week, new_weekly_grades, weights)
450
+
451
+ if verbose:
452
+ max_key_length = max(len(k) for k in weights.keys())
453
+ for k, v in avg_grades_dict.items():
454
+ print(f'{k:<{max_key_length}}:\t {v:.2f}')
417
455
 
418
- # Get average until current week
419
- new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
456
+ return new_weekly_grades # get rid of test and running avg columns
457
+ def get_all_students(admin_user, admin_pw):
458
+ res = requests.get(
459
+ url=api_base_url.rstrip("/") + "/students",
460
+ auth=HTTPBasicAuth(admin_user, admin_pw),
461
+ )
462
+ res.raise_for_status()
420
463
 
421
- # make new dataframe with the midterm, final, and running average
422
- max_key_length = max(len(k) for k in weights.keys())
423
- total = 0
424
- for k, v in weights.items():
425
- grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
426
- total += grade * v
427
- print(f"{k:<{max_key_length}}:\t {grade:.2f}")
428
- print(f"\nTotal: {total}") # exclude midterm and final
464
+ # Input: List of players
465
+ return [student['email'].split('@')[0] for student in res.json()]
429
466
 
430
- return new_weekly_grades # get rid of test and running avg columns
467
+
468
+ # def all_student_grades_testing(admin_user, admin_pw, start_date="2025-01-06"):
469
+ # """takes in json.
470
+ # reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
471
+ # fills in 0 for missing assignments
472
+ # calculate running average of each category"""
473
+
474
+ # # set up new df format
475
+ # weights = {
476
+ # "homework": 0.15,
477
+ # "lab": 0.15,
478
+ # "lecture": 0.15,
479
+ # "quiz": 0.15,
480
+ # "readings": 0.15,
481
+ # # 'midterm':0.15, 'final':0.2
482
+ # "labattendance": 0.05,
483
+ # "practicequiz": 0.05,
484
+ # }
485
+
486
+ # student_usernames = get_student_usernames(admin_user, admin_pw)
487
+
488
+ # assignments, student_subs = get_assignments_submissions(admin_user, admin_pw)
489
+
490
+ # new_grades_df = setup_grades_df(assignments)
491
+
492
+ # new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
493
+
494
+ # current_week = get_current_week(start_date)
495
+
496
+ # # Get average until current week
497
+ # new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
498
+
499
+ # # make new dataframe with the midterm, final, and running average
500
+ # max_key_length = max(len(k) for k in weights.keys())
501
+ # total = 0
502
+ # for k, v in weights.items():
503
+ # grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
504
+ # total += grade * v
505
+ # print(f"{k:<{max_key_length}}:\t {grade:.2f}")
506
+ # print(f"\nTotal: {total}") # exclude midterm and final
507
+
508
+ # return new_weekly_grades # get rid of test and running avg columns
509
+
@@ -0,0 +1,53 @@
1
+ import panel as pn
2
+ import requests
3
+ from requests.auth import HTTPBasicAuth
4
+ import os
5
+
6
+ from ..utils import api_base_url
7
+
8
+ # Dummy credentials for HTTP Basic Auth
9
+ AUTH = HTTPBasicAuth("user", "password")
10
+
11
+ # Panel configuration
12
+ pn.extension()
13
+
14
+
15
+ def get_jhub_user():
16
+ """
17
+ Fetches the JupyterHub user from the environment.
18
+ """
19
+ jhub_user = os.getenv("JUPYTERHUB_USER")
20
+ if jhub_user is None:
21
+ raise ValueError("JupyterHub user not found")
22
+ return jhub_user
23
+
24
+
25
+ def get_students():
26
+
27
+ # Make the request
28
+ response = requests.get(
29
+ f"{api_base_url}students",
30
+ auth=HTTPBasicAuth("user", "pass"), # Basic Auth
31
+ params={"requester": get_jhub_user()}, # Query parameter
32
+ )
33
+
34
+ # Print response
35
+ if response.status_code == 200:
36
+ return [student["email"].split("@")[0] for student in response.json()]
37
+ else:
38
+ print(f"Error {response.status_code}: {response.text}")
39
+
40
+
41
+ def get_assignments():
42
+ # Make the request
43
+ response = requests.get(
44
+ f"{api_base_url}assignments",
45
+ auth=HTTPBasicAuth("user", "pass"), # Basic Auth
46
+ params={"requester": get_jhub_user()}, # Query parameter
47
+ )
48
+
49
+ # Print response
50
+ if response.status_code == 200:
51
+ return [assignment["title"] for assignment in response.json()]
52
+ else:
53
+ print(f"Error {response.status_code}: {response.text}")
@@ -6,19 +6,30 @@ from requests.auth import HTTPBasicAuth
6
6
  from ..utils import api_base_url
7
7
 
8
8
 
9
- def build_token_payload(token: str, duration: int) -> dict:
9
+ def build_token_payload(token: str, duration: int, **kwargs) -> dict:
10
+
11
+ student_id = kwargs.get("student_id", None)
12
+ assignment = kwargs.get("assignment", None)
13
+
10
14
  jhub_user = os.getenv("JUPYTERHUB_USER")
11
15
  if jhub_user is None:
12
16
  raise ValueError("JupyterHub user not found")
13
17
 
14
- return {
18
+ payload = {
15
19
  "value": token,
16
20
  "duration": duration,
17
21
  "requester": jhub_user,
18
22
  }
19
23
 
24
+ if student_id:
25
+ payload["student_id"] = student_id
26
+ if assignment:
27
+ payload["assignment"] = assignment
28
+
29
+ return payload
30
+
20
31
 
21
- def add_token(token: str, duration: int = 20) -> None:
32
+ def add_token(token: str, duration: int = 20, **kwargs) -> None:
22
33
  """
23
34
  Sends a POST request to mint a token
24
35
  """
@@ -27,7 +38,7 @@ def add_token(token: str, duration: int = 20) -> None:
27
38
  raise ValueError("Environment variable for API URL not set")
28
39
  url = api_base_url.rstrip("/") + "/tokens"
29
40
 
30
- payload = build_token_payload(token=token, duration=duration)
41
+ payload = build_token_payload(token=token, duration=duration, **kwargs)
31
42
 
32
43
  # Dummy credentials for HTTP Basic Auth
33
44
  auth = HTTPBasicAuth("user", "password")
@@ -6,6 +6,16 @@ import requests
6
6
  from requests.auth import HTTPBasicAuth
7
7
 
8
8
 
9
+ def get_jhub_user():
10
+ """
11
+ Fetches the JupyterHub user from the environment.
12
+ """
13
+ jhub_user = os.getenv("JUPYTERHUB_USER")
14
+ if jhub_user is None:
15
+ raise ValueError("JupyterHub user not found")
16
+ return jhub_user
17
+
18
+
9
19
  class TokenValidationError(Exception):
10
20
  """
11
21
  Custom exception raised when the token validation fails.
@@ -41,7 +51,10 @@ def get_credentials() -> dict[str, str]:
41
51
  return {"username": username, "password": password}
42
52
 
43
53
 
44
- def validate_token(token: Optional[str] = None) -> None:
54
+ def validate_token(
55
+ token: Optional[str] = None,
56
+ assignment: Optional[str] = None,
57
+ ) -> None:
45
58
  if token:
46
59
  os.environ["TOKEN"] = token # If token passed, set env var
47
60
  else:
@@ -55,8 +68,17 @@ def validate_token(token: Optional[str] = None) -> None:
55
68
  if not base_url:
56
69
  print("Error: Environment variable 'DB_URL' not set", file=sys.stderr)
57
70
  sys.exit(1)
71
+
72
+ # Construct endpoint with optional parameters
58
73
  endpoint = f"{base_url.rstrip('/')}/validate-token/{token}"
59
74
 
75
+ # Build query parameters
76
+ params = {}
77
+ if assignment:
78
+ params["assignment"] = assignment
79
+
80
+ params["student_id"] = get_jhub_user()
81
+
60
82
  # Get credentials
61
83
  try:
62
84
  credentials = get_credentials()
@@ -69,7 +91,10 @@ def validate_token(token: Optional[str] = None) -> None:
69
91
  basic_auth = HTTPBasicAuth(username, password)
70
92
 
71
93
  try:
72
- response = requests.get(url=endpoint, auth=basic_auth, timeout=10)
94
+ # Send request with optional query parameters
95
+ response = requests.get(
96
+ url=endpoint, auth=basic_auth, timeout=10, params=params
97
+ )
73
98
  response.raise_for_status()
74
99
 
75
100
  detail = response.json().get("detail", response.text)