PyKubeGrader 0.3.2__py3-none-any.whl → 0.3.4__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.2
3
+ Version: 0.3.4
4
4
  Summary: Add a short description here!
5
5
  Home-page: https://github.com/pyscaffold/pyscaffold/
6
6
  Author: jagar2
@@ -12,12 +12,10 @@ Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Programming Language :: Python
13
13
  Description-Content-Type: text/x-rst; charset=UTF-8
14
14
  License-File: LICENSE.txt
15
- Requires-Dist: httpx
16
15
  Requires-Dist: importlib-metadata; python_version < "3.8"
17
16
  Requires-Dist: ipython
18
17
  Requires-Dist: mypy
19
18
  Requires-Dist: nbformat
20
- Requires-Dist: nest_asyncio
21
19
  Requires-Dist: numpy
22
20
  Requires-Dist: pandas-stubs
23
21
  Requires-Dist: panel
@@ -28,7 +26,6 @@ Requires-Dist: ruff
28
26
  Requires-Dist: setuptools
29
27
  Requires-Dist: sphinx
30
28
  Requires-Dist: types-python-dateutil
31
- Requires-Dist: types-pyyaml
32
29
  Requires-Dist: types-requests
33
30
  Requires-Dist: types-setuptools
34
31
  Provides-Extra: testing
@@ -1,21 +1,22 @@
1
1
  pykubegrader/__init__.py,sha256=AoAkdfIjDDZGWLlsIRENNq06L9h46kDGBIE8vRmsCfg,311
2
- pykubegrader/grading_tester.ipynb,sha256=2c1JgnV-zgP405Q3rlEkOlMJPm_9Qga0Z0MVWpFk5No,18531
2
+ pykubegrader/grading_tester.ipynb,sha256=wwT9jyhpR6GGM8r4todaGfrsUxS6JxM0qIqMcDYKM7w,18839
3
3
  pykubegrader/initialize.py,sha256=Bwu1q18l18FB9lGppvt-L41D5gzr3S8t6zC0_UbrASw,3994
4
- pykubegrader/telemetry.py,sha256=50Qp5WXeF7PD5FxDLFXWFAnQ2Yobj-wL3Dxh0Hz_vh0,6552
4
+ pykubegrader/telemetry.py,sha256=ooLK-dY_hJQ7t4r83hWyO8wx6F_7TfWJS7tCp_nH7r8,13049
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
8
  pykubegrader/build/api_notebook_builder.py,sha256=dlcVrGgsvxnt6GlAUN3e-FrpsPNJKXSHni1fstRCBik,20311
9
9
  pykubegrader/build/build_folder.py,sha256=Asc-VdhXgxQfOfFIWJShhXrF2EITJOIZQ5Dz_2y-P2I,85358
10
10
  pykubegrader/build/clean_folder.py,sha256=8N0KyL4eXRs0DCw-V_2jR9igtFs_mOFMQufdL6tD-38,1323
11
+ pykubegrader/build/collate.py,sha256=cVvF7tf2U3iiH4R_dbghTcieedIx5w3Fyw9L_llInM8,6754
11
12
  pykubegrader/build/markdown_questions.py,sha256=cSh8mkHK3hh-etJdgrZu9UQi1WPrKQtofkzLCUp1Z-w,4676
12
- pykubegrader/grade_reports/grade_reports.py,sha256=TmT_F2enezU_BIFPBQ0oZ4l5nBgPw4PSBo-8wJ_gt4w,5915
13
+ pykubegrader/grade_reports/grade_reports.py,sha256=n8H_n9jdZRSPn2zlIf-GQt_Y8w91p6M8ZbdVH76Sg5k,2303
13
14
  pykubegrader/graders/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
14
15
  pykubegrader/graders/late_assignments.py,sha256=_2-rA5RqO0BWY9WAQA_mbCxxPKTOiJOl-byD2CYWaE0,1393
15
16
  pykubegrader/log_parser/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
16
17
  pykubegrader/log_parser/parse.ipynb,sha256=5e-9dzUbJk2M8kPP55lVeksm86lSY5ocKfWOP2RSWH0,11921
17
18
  pykubegrader/log_parser/parse.py,sha256=dXzTEOTI6VTRNoHFDAjg6hZUhvB3kHtMb10_KW3NPrw,7641
18
- pykubegrader/submit/submit_assignment.py,sha256=UgJXKWw5b8-bRSFnba4iHAyXnujULHcWIask7hKx9ik,3421
19
+ pykubegrader/submit/submit_assignment.py,sha256=cqVu7US8GVaCdJdaU2yjawlVBtAKP5XJc4oAvX5FeRU,2575
19
20
  pykubegrader/tokens/tokens.py,sha256=X9f3SzrGCrAJp_BXhr6VJn5f0LxtgQ7HLPBw7zEF2BY,1198
20
21
  pykubegrader/tokens/validate_token.py,sha256=MQtgz_USvSZ9JahJ48ybjp74F5aYz64lhtvuwVc4kQw,2712
21
22
  pykubegrader/widgets/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
@@ -31,9 +32,9 @@ pykubegrader/widgets_base/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-
31
32
  pykubegrader/widgets_base/multi_select.py,sha256=JgjhHQJL8Pf0-1T_wdZCecAK1IgVJrZBCbR6b3jvDtk,4181
32
33
  pykubegrader/widgets_base/reading.py,sha256=ChUS3NOTa_HLtNpxR8hGX80LPKMvYMypnR6dFknfxus,5430
33
34
  pykubegrader/widgets_base/select.py,sha256=tEDg7GEjsZnz1646YTthTeamujVRS5jDJWMsXhmOQbI,2705
34
- PyKubeGrader-0.3.2.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
35
- PyKubeGrader-0.3.2.dist-info/METADATA,sha256=le0n8bDcxtGgZ-XG4-2YXPVpq6nDgai_EKdeRdcDLcg,2806
36
- PyKubeGrader-0.3.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
37
- PyKubeGrader-0.3.2.dist-info/entry_points.txt,sha256=BbLXpFZObpOXA8e3p3GcFkL-sHdUnDLUcnYmc6zx3NI,201
38
- PyKubeGrader-0.3.2.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
39
- PyKubeGrader-0.3.2.dist-info/RECORD,,
35
+ PyKubeGrader-0.3.4.dist-info/LICENSE.txt,sha256=YTp-Ewc8Kems8PJEE27KnBPFnZSxoWvSg7nnknzPyYw,1546
36
+ PyKubeGrader-0.3.4.dist-info/METADATA,sha256=6aq3PWnDPR8lNxPMmWvDkbd2GvZNfLkpYNcpOLSbHqc,2729
37
+ PyKubeGrader-0.3.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
38
+ PyKubeGrader-0.3.4.dist-info/entry_points.txt,sha256=RR57KvzDRJrP4omy5heS5cZ3E7g56YxcxJhDnp57ZU0,253
39
+ PyKubeGrader-0.3.4.dist-info/top_level.txt,sha256=e550Klfze6higFxER1V62fnGOcIgiKRbsrl9CC4UdtQ,13
40
+ PyKubeGrader-0.3.4.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
+ collate-questions = pykubegrader.build.collate:main
2
3
  markdown-question = pykubegrader.build.markdown_questions:main
3
4
  otter-folder-builder = pykubegrader.build.build_folder:main
4
5
  otter-folder-cleaner = pykubegrader.build.clean_folder:main
@@ -0,0 +1,190 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+
5
+ from nbformat.v4 import new_markdown_cell, new_notebook
6
+
7
+
8
+ class QuestionCollator:
9
+ def __init__(self, root_folder: str, output_path: str):
10
+ """
11
+ Initializes the QuestionCollator with the root folder and output path.
12
+
13
+ Args:
14
+ root_folder (str): Path to the root folder containing the solution files.
15
+ output_path (str): Path to save the collated notebook.
16
+ """
17
+ self.root_folder = root_folder
18
+ self.output_path = output_path
19
+
20
+ def find_solution_folders(self):
21
+ """
22
+ Finds all immediate subdirectories inside '_solution*' folders that contain notebooks.
23
+
24
+ Returns:
25
+ list: List of folder paths containing notebooks.
26
+ """
27
+ solution_folders = []
28
+
29
+ # Look for _solution* folders inside the root_folder
30
+ for dir_name in os.listdir(self.root_folder):
31
+ solution_folder_path = os.path.join(self.root_folder, dir_name)
32
+
33
+ if os.path.isdir(solution_folder_path) and dir_name.startswith("_solution"):
34
+ print(f"Found solution folder: {solution_folder_path}") # Debug output
35
+
36
+ # Now, look for immediate subdirectories inside this _solution* folder
37
+ for sub_dir in os.listdir(solution_folder_path):
38
+ sub_dir_path = os.path.join(solution_folder_path, sub_dir)
39
+
40
+ if os.path.isdir(sub_dir_path):
41
+ # Check if this subdirectory contains at least one .ipynb file
42
+ if any(f.endswith(".ipynb") for f in os.listdir(sub_dir_path)):
43
+ solution_folders.append(sub_dir_path)
44
+
45
+ print(f"Final list of solution subfolders: {solution_folders}") # Debug output
46
+ return solution_folders
47
+
48
+ def extract_questions(self, folder_path):
49
+ """
50
+ Extracts questions from all notebooks in the solution folder.
51
+
52
+ Args:
53
+ folder_path (str): Path to the solution folder.
54
+
55
+ Returns:
56
+ dict: Dictionary of categorized questions.
57
+ """
58
+ questions = {
59
+ "multiple_choice": [],
60
+ "select_many": [],
61
+ "true_false": [],
62
+ "other": [],
63
+ }
64
+
65
+ for file in os.listdir(folder_path):
66
+ if file.endswith(".ipynb"):
67
+ file_path = os.path.join(folder_path, file)
68
+ print(
69
+ f"Processing notebook: {file_path}"
70
+ ) # Print the full path of the notebook
71
+ with open(file_path, "r") as f:
72
+ content = json.load(f)
73
+
74
+ # Track whether we are inside a question block
75
+ in_question_block = False
76
+ current_question_content = []
77
+
78
+ for cell in content["cells"]:
79
+ if "# BEGIN MULTIPLE CHOICE" in cell["source"]:
80
+ # Start of a question block
81
+ in_question_block = True
82
+ current_question_content = []
83
+ elif "# END MULTIPLE CHOICE" in cell["source"]:
84
+ # End of a question block
85
+ in_question_block = False
86
+ if current_question_content:
87
+ questions["multiple_choice"].append(
88
+ {"source": "\n".join(current_question_content)}
89
+ )
90
+ current_question_content = []
91
+ elif in_question_block and cell["cell_type"] == "markdown":
92
+ # Capture markdown cells within the question block
93
+ current_question_content.append(cell["source"])
94
+
95
+ return questions
96
+
97
+ def create_collated_notebook(self, questions):
98
+ """
99
+ Creates a new notebook with questions organized by type.
100
+
101
+ Args:
102
+ questions (dict): Dictionary of categorized questions.
103
+
104
+ Returns:
105
+ Notebook: The collated notebook.
106
+ """
107
+ nb = new_notebook()
108
+
109
+ # Add Multiple Choice Questions
110
+ nb.cells.append(new_markdown_cell("# Multiple Choice Questions"))
111
+ for q in questions["multiple_choice"]:
112
+ nb.cells.append(new_markdown_cell(q["source"]))
113
+
114
+ # Add Select Many Questions
115
+ nb.cells.append(new_markdown_cell("# Select Many Questions"))
116
+ for q in questions["select_many"]:
117
+ nb.cells.append(new_markdown_cell(q["source"]))
118
+
119
+ # Add True/False Questions
120
+ nb.cells.append(new_markdown_cell("# True/False Questions"))
121
+ for q in questions["true_false"]:
122
+ nb.cells.append(new_markdown_cell(q["source"]))
123
+
124
+ # Add Other Questions
125
+ nb.cells.append(new_markdown_cell("# Other Questions"))
126
+ for q in questions["other"]:
127
+ nb.cells.append(new_markdown_cell(q["source"]))
128
+
129
+ return nb
130
+
131
+ def save_notebook(self, nb):
132
+ """
133
+ Saves the collated notebook to the specified output path.
134
+
135
+ Args:
136
+ nb (Notebook): The notebook to save.
137
+ """
138
+ import nbformat
139
+
140
+ with open(self.output_path, "w") as f:
141
+ nbformat.write(nb, f)
142
+
143
+ def collate_questions(self):
144
+ """
145
+ Collates questions from all solution folders and saves them to a new notebook.
146
+ """
147
+ solution_folders = self.find_solution_folders()
148
+ all_questions = {
149
+ "multiple_choice": [],
150
+ "select_many": [],
151
+ "true_false": [],
152
+ "other": [],
153
+ }
154
+
155
+ for folder in solution_folders:
156
+ questions = self.extract_questions(folder)
157
+ all_questions["multiple_choice"].extend(questions["multiple_choice"])
158
+ all_questions["select_many"].extend(questions["select_many"])
159
+ all_questions["true_false"].extend(questions["true_false"])
160
+ all_questions["other"].extend(questions["other"])
161
+
162
+ collated_nb = self.create_collated_notebook(all_questions)
163
+ self.save_notebook(collated_nb)
164
+ print(f"Collated notebook saved to {self.output_path}")
165
+
166
+
167
+ def main():
168
+ parser = argparse.ArgumentParser(
169
+ description="Collate questions from solution folders into a single notebook."
170
+ )
171
+ parser.add_argument(
172
+ "root_folder",
173
+ type=str,
174
+ help="Path to the root folder containing solution folders",
175
+ )
176
+ parser.add_argument(
177
+ "output_path", type=str, help="Path to save the collated notebook"
178
+ )
179
+
180
+ args = parser.parse_args()
181
+ collator = QuestionCollator(
182
+ root_folder=args.root_folder, output_path=args.output_path
183
+ )
184
+ collator.collate_questions()
185
+
186
+
187
+ if __name__ == "__main__":
188
+ import sys
189
+
190
+ sys.exit(main())
@@ -1,9 +1,9 @@
1
+ import os
2
+
1
3
  import pandas as pd
2
4
  import requests
3
5
  from requests.auth import HTTPBasicAuth
4
6
 
5
- from ..build.passwords import password, user
6
-
7
7
 
8
8
  def format_assignment_table(assignments):
9
9
  # Create DataFrame
@@ -32,16 +32,21 @@ def format_assignment_table(assignments):
32
32
  return df
33
33
 
34
34
 
35
- def get_student_grades(
36
- student_username, api_base_url="https://engr-131-api.eastus.cloudapp.azure.com/"
37
- ):
35
+ def get_student_grades(student_username):
36
+ # Get env variables here, in the function, rather than globally
37
+ api_base_url = os.getenv("DB_URL")
38
+ student_user = os.getenv("user_name_student")
39
+ student_pw = os.getenv("keys_student")
40
+
41
+ if not api_base_url or not student_user or not student_pw:
42
+ raise ValueError("Environment variables not set")
43
+
38
44
  params = {"username": student_username}
39
45
  res = requests.get(
40
46
  url=api_base_url.rstrip("/") + "/student-grades-testing",
41
47
  params=params,
42
- auth=HTTPBasicAuth(user(), password()),
48
+ auth=HTTPBasicAuth(student_user, student_pw),
43
49
  )
44
-
45
50
  [assignments, sub] = res.json()
46
51
 
47
52
  assignments_df = format_assignment_table(assignments)
@@ -69,103 +74,3 @@ def filter_assignments(df, max_week=None, exclude_types=None):
69
74
  df = df[~df["assignment_type"].isin(exclude_types)]
70
75
 
71
76
  return df
72
-
73
-
74
- # import os
75
- # import numpy as np
76
- # import pandas as pd
77
- # import socket
78
- # import requests
79
- # from IPython.core.interactiveshell import ExecutionInfo
80
- # from requests import Response
81
- # from requests.auth import HTTPBasicAuth
82
- # from requests.exceptions import RequestException
83
- # from pykubegrader.graders.late_assignments import calculate_late_submission
84
-
85
-
86
- # api_base_url = os.getenv("DB_URL")
87
- # student_user = "admin" # os.getenv("user_name_student")
88
- # student_pw = "TrgpUuadm2PWtdgtC7Yt" # os.getenv("keys_student")
89
-
90
- # from_hostname = socket.gethostname().removeprefix("jupyter-")
91
- # from_env = os.getenv("JUPYTERHUB_USER")
92
- # params = {"username": from_env}
93
-
94
- # letteronly = lambda s: re.sub(r'[^a-zA-Z]', '', s)
95
- # start_date='2025-01-06'
96
-
97
- # # get assignment information
98
- # res = requests.get(
99
- # url=api_base_url.rstrip("/") + "/assignments",
100
- # auth=HTTPBasicAuth(student_user, student_pw),)
101
- # res.raise_for_status()
102
- # assignments = res.json()
103
-
104
- # # get submission information
105
- # res = requests.get(
106
- # url=api_base_url.rstrip("/") + "/testing/get-all-assignment-subs",
107
- # auth=HTTPBasicAuth('testing', 'Vok8WzmuCMVYULw3tqzJ'),
108
- # )
109
- # subs = res.json()
110
- # student_subs = [sub for sub in subs if sub['student_email']==from_env]
111
-
112
- # # set up new df format
113
- # weights = {'homework':0.15, 'lab':0.15, 'lecture':0.15, 'quiz':0.15, 'readings':0.15,
114
- # # 'midterm':0.15, 'final':0.2
115
- # 'labattendance':0.05, 'practicequiz':0.05, }
116
- # assignment_types = list(set([a['assignment_type'] for a in assignments]))+['Running Avg']
117
- # inds = [f'week{i+1}' for i in range(11)]+['Running Avg']
118
- # restruct_grades = {k: np.zeros(len(inds)) for k in assignment_types}
119
- # restruct_grades['inds']=inds
120
- # new_weekly_grades = pd.DataFrame(restruct_grades)
121
-
122
- # for assignment in assignments:
123
- # # get the assignment from all submissions
124
- # subs = [ sub for sub in student_subs if \
125
- # letteronly(sub['assignment_type'])==letteronly(assignment['assignment_type']) and \
126
- # sub['week_number']==assignment['week_number'] ]
127
- # if len(subs)==0: continue
128
- # if len(subs)>1:
129
-
130
- # # get due date from assignment
131
- # due_date = datetime.datetime.strptime(assignment['due_date'], "%Y-%m-%d %H:%M:%S")
132
- # for sub in subs:
133
- # entry_date = datetime.strptime(sub['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
134
- # if entry_date <= due_date:
135
- # else after_due).append(entry)
136
- # calculate_late_submission(due = due_date, # '2025-01-21T18:59:59Z'.
137
- # submitted = subs"%Y-%m-%d %H:%M:%S".
138
- # - Q0 (float): Initial value (default is 100).
139
- # - Q_min (float): Minimum value (default is 40).
140
- # - k (float): Decay constant per minute (default is 6.88e-5).
141
-
142
- # # get max from before due date
143
-
144
- # # get max score from after due date and calculate
145
- # print(sub['assignment'])
146
- # print(subs)
147
- # return
148
- # # fill out grades
149
- # new_weekly_grades.set_index('inds',inplace=True)
150
- # splitted = [col_name.split('-')+[grades[col_name][0]] for col_name in grades.columns]
151
- # for week,assignment,grade in splitted: new_weekly_grades.loc[week,assignment] = grade
152
-
153
- # # Calculate the current week (1-based indexing)
154
- # start_date = datetime.strptime(start_date, "%Y-%m-%d")
155
- # today = datetime.now()
156
- # days_since_start = (today - start_date).days
157
- # current_week = days_since_start // 7 + 1
158
-
159
- # # Get average until current week
160
- # new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[:current_week-1].mean()
161
-
162
- # # make new dataframe with the midterm, final, and running average
163
- # max_key_length = max(len(k) for k in weights.keys())
164
- # total = 0
165
- # for k, v in weights.items():
166
- # grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
167
- # total+=grade*v
168
- # print(f'{k:<{max_key_length}}:\t {grade:.2f}')
169
- # print(f'\nTotal: {total}') # exclude midterm and final
170
-
171
- # return new_out
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from grade_reports.grade_reports import filter_assignments, get_student_grades\n",
10
+ "\n",
11
+ "get_student_grades"
12
+ ]
13
+ },
3
14
  {
4
15
  "cell_type": "code",
5
16
  "execution_count": null,
@@ -19,13 +30,13 @@
19
30
  "api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\"\n",
20
31
  "\n",
21
32
  "\n",
22
- "def get_all_students():\n",
23
- " res = requests.get(\n",
24
- " url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
25
- " auth=HTTPBasicAuth(user(), password()),\n",
26
- " )\n",
33
+ "# def get_all_students():\n",
34
+ "# res = requests.get(\n",
35
+ "# url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
36
+ "# auth=HTTPBasicAuth(user(), password()),\n",
37
+ "# )\n",
27
38
  "\n",
28
- " return res.json()\n",
39
+ "# return res.json()\n",
29
40
  "\n",
30
41
  "\n",
31
42
  "# def get_student_grades(student_id):\n",
@@ -41,47 +52,47 @@
41
52
  "# return assignments, sub\n",
42
53
  "\n",
43
54
  "\n",
44
- "def get_student(student):\n",
45
- " print(student)\n",
46
- " # Get assignments and submissions for the student (assumed functions)\n",
47
- " assignments, submissions = get_student_grades(student)\n",
55
+ "# def get_student(student):\n",
56
+ "# print(student)\n",
57
+ "# # Get assignments and submissions for the student (assumed functions)\n",
58
+ "# assignments, submissions = get_student_grades(student)\n",
48
59
  "\n",
49
- " # Recalculate grades and get a grades dictionary\n",
50
- " grades_dict = recalculate_best_grades(assignments, submissions)\n",
60
+ "# # Recalculate grades and get a grades dictionary\n",
61
+ "# grades_dict = recalculate_best_grades(assignments, submissions)\n",
51
62
  "\n",
52
- " # Calculate averages and build a row for the student\n",
53
- " row = calculate_averages(grades_dict, student)\n",
63
+ "# # Calculate averages and build a row for the student\n",
64
+ "# row = calculate_averages(grades_dict, student)\n",
54
65
  "\n",
55
- " # Convert the row (a dictionary) into a DataFrame\n",
56
- " # row_df = pd.DataFrame([row])\n",
66
+ "# # Convert the row (a dictionary) into a DataFrame\n",
67
+ "# # row_df = pd.DataFrame([row])\n",
57
68
  "\n",
58
- " return row\n",
69
+ "# return row\n",
59
70
  "\n",
60
71
  "\n",
61
- "def get_all_student_grades():\n",
62
- " # Initialize an empty DataFrame to hold all student grades\n",
63
- " df = pd.DataFrame()\n",
72
+ "# def get_all_student_grades():\n",
73
+ "# # Initialize an empty DataFrame to hold all student grades\n",
74
+ "# df = pd.DataFrame()\n",
64
75
  "\n",
65
- " # Get all students (assuming get_all_students() is a defined function)\n",
66
- " students = get_all_students()\n",
76
+ "# # Get all students (assuming get_all_students() is a defined function)\n",
77
+ "# students = get_all_students()\n",
67
78
  "\n",
68
- " for student in students:\n",
69
- " row_df = get_student(student)\n",
79
+ "# for student in students:\n",
80
+ "# row_df = get_student(student)\n",
70
81
  "\n",
71
- " # Append the row to the DataFrame\n",
72
- " df = pd.concat([df, row_df], ignore_index=True)\n",
82
+ "# # Append the row to the DataFrame\n",
83
+ "# df = pd.concat([df, row_df], ignore_index=True)\n",
73
84
  "\n",
74
- " return df\n",
85
+ "# return df\n",
75
86
  "\n",
76
87
  "\n",
77
- "def get_max_deadline(assignments, assignment_name, week_number):\n",
78
- " matching_rows = assignments[\n",
79
- " (assignments[\"week_number\"] == week_number)\n",
80
- " & (assignments[\"assignment_name\"] == assignment_name)\n",
81
- " ]\n",
88
+ "# def get_max_deadline(assignments, assignment_name, week_number):\n",
89
+ "# matching_rows = assignments[\n",
90
+ "# (assignments[\"week_number\"] == week_number)\n",
91
+ "# & (assignments[\"assignment_name\"] == assignment_name)\n",
92
+ "# ]\n",
82
93
  "\n",
83
- " max_timestamp = matching_rows[\"due_date\"].max()\n",
84
- " return max_timestamp\n",
94
+ "# max_timestamp = matching_rows[\"due_date\"].max()\n",
95
+ "# return max_timestamp\n",
85
96
  "\n",
86
97
  "\n",
87
98
  "def calculate_averages(grades_dict, student_id):\n",
@@ -455,7 +466,7 @@
455
466
  ],
456
467
  "metadata": {
457
468
  "kernelspec": {
458
- "display_name": "engr131_dev",
469
+ "display_name": "engr131w25",
459
470
  "language": "python",
460
471
  "name": "python3"
461
472
  },
@@ -469,7 +480,7 @@
469
480
  "name": "python",
470
481
  "nbconvert_exporter": "python",
471
482
  "pygments_lexer": "ipython3",
472
- "version": "3.12.7"
483
+ "version": "3.13.1"
473
484
  }
474
485
  },
475
486
  "nbformat": 4,
@@ -1,12 +1,7 @@
1
- import asyncio
2
- import base64
3
1
  import os
4
2
 
5
- import httpx
6
- import nest_asyncio # type: ignore
7
-
8
- # Apply nest_asyncio for environments like Jupyter
9
- nest_asyncio.apply()
3
+ import requests
4
+ from requests.auth import HTTPBasicAuth
10
5
 
11
6
 
12
7
  def get_credentials():
@@ -22,55 +17,52 @@ def get_credentials():
22
17
  return {"username": username, "password": password}
23
18
 
24
19
 
25
- async def call_score_assignment(
20
+ def call_score_assignment(
26
21
  assignment_title: str, notebook_title: str, file_path: str = ".output_reduced.log"
27
- ) -> dict:
22
+ ) -> dict[str, str]:
28
23
  """
29
- Submit an assignment to the scoring endpoint.
24
+ Submit an assignment to the scoring endpoint
30
25
 
31
26
  Args:
32
- assignment_title (str): Title of the assignment.
33
- file_path (str): Path to the log file to upload.
27
+ assignment_title (str): Title of the assignment
28
+ notebook_title (str): Title of the notebook
29
+ file_path (str): Path to the log file to upload
34
30
 
35
31
  Returns:
36
- dict: JSON response from the server.
32
+ dict: JSON response from the server
37
33
  """
38
- # Fetch the endpoint URL from environment variables
34
+
39
35
  base_url = os.getenv("DB_URL")
40
36
  if not base_url:
41
- raise ValueError("Environment variable 'DB_URL' is not set.")
42
- url = f"{base_url}score-assignment?assignment_title={assignment_title}&notebook_title={notebook_title}"
43
-
44
- # Get credentials
45
- credentials = get_credentials()
46
- username = credentials["username"]
47
- password = credentials["password"]
48
-
49
- # Encode credentials for Basic Authentication
50
- auth_header = (
51
- f"Basic {base64.b64encode(f'{username}:{password}'.encode()).decode()}"
52
- )
53
-
54
- # Send the POST request
55
- async with httpx.AsyncClient() as client:
56
- try:
57
- with open(file_path, "rb") as file:
58
- response = await client.post(
59
- url,
60
- headers={"Authorization": auth_header}, # Add Authorization header
61
- files={"log_file": file}, # Upload log file
62
- )
63
-
64
- # Handle the response
65
- response.raise_for_status() # Raise an exception for HTTP errors
66
- response_data = response.json()
67
- return response_data
68
- except FileNotFoundError:
69
- raise FileNotFoundError(f"The file {file_path} does not exist.")
70
- except httpx.RequestError as e:
71
- raise RuntimeError(f"An error occurred while requesting {url}: {e}")
72
- except Exception as e:
73
- raise RuntimeError(f"An unexpected error occurred: {e}")
37
+ raise ValueError("Environment variable 'DB_URL' not set")
38
+
39
+ url = base_url.rstrip("/") + "/score-assignment"
40
+
41
+ params = {
42
+ "assignment_title": assignment_title,
43
+ "notebook_title": notebook_title,
44
+ }
45
+
46
+ username, password = get_credentials().values()
47
+
48
+ try:
49
+ with open(file_path, "rb") as file:
50
+ res = requests.post(
51
+ url=url,
52
+ params=params,
53
+ auth=HTTPBasicAuth(username, password),
54
+ files={"log_file": file},
55
+ )
56
+ res.raise_for_status()
57
+
58
+ return res.json()
59
+
60
+ except FileNotFoundError:
61
+ raise FileNotFoundError(f"File {file_path} does not exist")
62
+ except requests.RequestException as err:
63
+ raise RuntimeError(f"An error occurred while requesting {url}: {err}")
64
+ except Exception as err:
65
+ raise RuntimeError(f"An unexpected error occurred: {err}")
74
66
 
75
67
 
76
68
  def submit_assignment(
@@ -85,17 +77,9 @@ def submit_assignment(
85
77
  assignment_title (str): Title of the assignment.
86
78
  file_path (str): Path to the log file to upload.
87
79
  """
88
- # Get the current event loop or create one
89
- try:
90
- loop = asyncio.get_event_loop()
91
- except RuntimeError:
92
- loop = asyncio.new_event_loop()
93
- asyncio.set_event_loop(loop)
94
-
95
- # Run the async function in the event loop
96
- response = loop.run_until_complete(
97
- call_score_assignment(assignment_title, notebook_title, file_path)
98
- )
80
+
81
+ response = call_score_assignment(assignment_title, notebook_title, file_path)
82
+
99
83
  print("Server Response:", response.get("message", "No message in response"))
100
84
 
101
85
 
pykubegrader/telemetry.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import datetime
3
+ import gzip
3
4
  import json
4
5
  import logging
5
6
  import os
@@ -9,11 +10,13 @@ from typing import Any, Optional
9
10
  import nacl.public
10
11
  import pandas as pd
11
12
  import requests
13
+ from dateutil import parser
12
14
  from IPython.core.interactiveshell import ExecutionInfo
13
15
  from requests import Response
14
16
  from requests.auth import HTTPBasicAuth
15
17
  from requests.exceptions import RequestException
16
18
 
19
+ from .graders.late_assignments import calculate_late_submission
17
20
  from .utils import api_base_url, student_pw, student_user
18
21
 
19
22
  #
@@ -205,10 +208,14 @@ def verify_server(jhub_user: Optional[str] = None) -> str:
205
208
  return message
206
209
 
207
210
 
211
+ # TODO: reformat into a nice table
208
212
  def get_my_grades() -> pd.DataFrame:
213
+ # get all submissions,
214
+ # recalculate late penalty in new columns,
215
+ # take max,
216
+ # divide by total points
209
217
  if not student_user or not student_pw or not api_base_url:
210
218
  raise ValueError("Necessary environment variables not set")
211
-
212
219
  from_hostname = socket.gethostname().removeprefix("jupyter-")
213
220
  from_env = os.getenv("JUPYTERHUB_USER")
214
221
  if from_hostname != from_env:
@@ -233,3 +240,191 @@ def get_my_grades() -> pd.DataFrame:
233
240
  sorted_vertical_df = vertical_df.sort_index()
234
241
 
235
242
  return sorted_vertical_df
243
+
244
+
245
+ #
246
+ # Code execution log testing
247
+ #
248
+
249
+
250
+ def upload_execution_log() -> None:
251
+ if not student_user or not student_pw or not api_base_url:
252
+ raise ValueError("Necessary environment variables not set")
253
+
254
+ responses = ensure_responses()
255
+ student_email: str = responses["jhub_user"]
256
+ assignment: str = responses["assignment"]
257
+ if not student_email or not assignment:
258
+ raise ValueError("Missing student email and/or assignment name")
259
+
260
+ print(f"Student: {student_email}")
261
+ print(f"Assignment: {assignment}")
262
+ print("Uploading code execution log...")
263
+
264
+ try:
265
+ with open(".output_code.log", "rb") as f:
266
+ log_bytes = f.read()
267
+ except FileNotFoundError:
268
+ raise FileNotFoundError("Code execution log not found")
269
+
270
+ print(f"Uncompressed log size: {len(log_bytes)} bytes")
271
+
272
+ compressed = gzip.compress(log_bytes)
273
+
274
+ print(f"Compressed log size: {len(compressed)} bytes")
275
+
276
+ encoded = base64.b64encode(compressed).decode("utf-8")
277
+
278
+ payload = {
279
+ "student_email": student_email,
280
+ "assignment": assignment,
281
+ "encrypted_content": encoded,
282
+ }
283
+
284
+ res = requests.post(
285
+ url=api_base_url.rstrip("/") + "/execution-logs",
286
+ json=payload,
287
+ auth=HTTPBasicAuth(student_user, student_pw),
288
+ )
289
+ res.raise_for_status()
290
+
291
+ print("Execution log uploaded successfully")
292
+
293
+
294
+ # #
295
+ # # Qiao's work on grades
296
+ #
297
+
298
+
299
+ def get_assignments_submissions():
300
+ if not student_user or not student_pw or not api_base_url:
301
+ raise ValueError("Necessary environment variables not set")
302
+ from_hostname = socket.gethostname().removeprefix("jupyter-")
303
+ from_env = os.getenv("JUPYTERHUB_USER")
304
+ if from_hostname != from_env:
305
+ raise ValueError("Problem with JupyterHub username")
306
+
307
+ params = {"username": from_env}
308
+ # get submission information
309
+ res = requests.get(
310
+ url=api_base_url.rstrip("/") + "/my-grades-testing",
311
+ params=params,
312
+ auth=HTTPBasicAuth(student_user, student_pw),
313
+ )
314
+ return res.json()
315
+
316
+
317
+ def setup_grades_df(assignments):
318
+ assignment_types = list(set([a["assignment_type"] for a in assignments]))
319
+
320
+ inds = [f"week{i + 1}" for i in range(11)] + ["Running Avg"]
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)
324
+ new_weekly_grades.set_index("inds", inplace=True)
325
+ return new_weekly_grades
326
+
327
+
328
+ def fill_grades_df(new_weekly_grades, assignments, student_subs):
329
+ for assignment in assignments:
330
+ # 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
+ ]
337
+ if len(subs) == 0:
338
+ # print(assignment['title'], 0, assignment['max_score'])
339
+ continue
340
+ elif len(subs) == 1:
341
+ grade = subs[0]["raw_score"] / assignment["max_score"]
342
+ # print(assignment['title'], sub['raw_score'], assignment['max_score'])
343
+ else:
344
+ # get due date from assignment
345
+ due_date = parser.parse(assignment["due_date"])
346
+ grades = []
347
+ for sub in subs:
348
+ entry_date = parser.parse(sub["timestamp"])
349
+ if entry_date <= due_date:
350
+ grades.append(sub["raw_score"])
351
+ else:
352
+ grades.append(
353
+ calculate_late_submission(
354
+ due_date.strftime("%Y-%m-%d %H:%M:%S"),
355
+ entry_date.strftime("%Y-%m-%d %H:%M:%S"),
356
+ )
357
+ )
358
+ # print(assignment['title'], grades, assignment['max_score'])
359
+ grade = max(grades) / assignment["max_score"]
360
+
361
+ # fill out new df with max
362
+ new_weekly_grades.loc[
363
+ f"week{assignment['week_number']}", assignment["assignment_type"]
364
+ ] = grade
365
+
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
381
+
382
+
383
+ def get_current_week(start_date):
384
+ # Calculate the current week (1-based indexing)
385
+ start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d")
386
+ today = datetime.datetime.now()
387
+ days_since_start = (today - start_date).days
388
+ return days_since_start // 7 + 1
389
+
390
+
391
+ # This function currently has many undefined variables and other problems!
392
+ def get_my_grades_testing(start_date="2025-01-06"):
393
+ """takes in json.
394
+ reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
395
+ fills in 0 for missing assignments
396
+ calculate running average of each category"""
397
+
398
+ # set up new df format
399
+ weights = {
400
+ "homework": 0.15,
401
+ "lab": 0.15,
402
+ "lecture": 0.15,
403
+ "quiz": 0.15,
404
+ "readings": 0.15,
405
+ # 'midterm':0.15, 'final':0.2
406
+ "labattendance": 0.05,
407
+ "practicequiz": 0.05,
408
+ }
409
+
410
+ assignments, student_subs = get_assignments_submissions()
411
+
412
+ new_grades_df = setup_grades_df(assignments)
413
+
414
+ new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
415
+
416
+ current_week = get_current_week(start_date)
417
+
418
+ # Get average until current week
419
+ new_weekly_grades.iloc[-1] = new_weekly_grades.iloc[: current_week - 1].mean()
420
+
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
429
+
430
+ return new_weekly_grades # get rid of test and running avg columns