PyKubeGrader 0.3.7__py3-none-any.whl → 0.3.9__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.
@@ -0,0 +1,137 @@
1
+ # # import the password and user from the build folder
2
+ # try:
3
+ # from pykubegrader.build.passwords import password, user
4
+ # except: # noqa: E722
5
+ # print("Passwords not found, cannot access database")
6
+
7
+ from pykubegrader.grade_reports.grading_config import assignment_type_list, skipped_users
8
+ from pykubegrader.grade_reports.grade_report import GradeReport
9
+ # from ..build.passwords import password, user
10
+ from pykubegrader.telemetry import get_all_students
11
+
12
+
13
+ import os
14
+ import requests
15
+ from requests.auth import HTTPBasicAuth
16
+ import socket
17
+ import pandas as pd
18
+ import numpy as np
19
+ import tqdm
20
+
21
+ # user = user()
22
+ # password = password()
23
+
24
+ # Set the environment variables for the database
25
+ # os.environ["JUPYTERHUB_USER"] = "jca92"
26
+ # os.environ["TOKEN"] = "token"
27
+ # os.environ["DB_URL"] = "https://engr-131-api.eastus.cloudapp.azure.com/"
28
+ # os.environ["keys_student"] = "capture"
29
+ # os.environ["user_name_student"] = "student"
30
+
31
+ api_base_url = os.getenv("DB_URL")
32
+ student_user = os.getenv("user_name_student")
33
+ student_pw = os.getenv("keys_student")
34
+
35
+
36
+ class ClassGradeReport:
37
+ """Generates and manages a class-wide grade report.
38
+
39
+ This class retrieves a list of students, initializes a structured grade report,
40
+ and populates it with individual student grade data. The final report includes
41
+ both assignment-specific grades and a weighted average grade.
42
+
43
+ Attributes:
44
+ student_list (list): A sorted list of all students in the class.
45
+ all_student_grades_df (pd.DataFrame): A DataFrame storing grades for each student,
46
+ including assignment scores and a weighted average.
47
+
48
+ Methods:
49
+ setup_class_grades():
50
+ Initializes an empty DataFrame with assignment names and weighted average columns.
51
+ update_student_grade(student):
52
+ Fetches and updates an individual student’s weighted average grades in the DataFrame.
53
+ fill_class_grades():
54
+ Iterates through all students to populate the class-wide grade report.
55
+ """
56
+
57
+ def __init__(self, user, password):
58
+ """Initializes the class grade report.
59
+
60
+ Retrieves the student list using authentication, sorts it, and sets up
61
+ the class-wide grade report by initializing and populating a DataFrame.
62
+ """
63
+ self.user = user
64
+ self.password = password
65
+
66
+ self.student_list = get_all_students(self.user, self.password)
67
+ self.student_list = list( set(self.student_list)-set(skipped_users) )
68
+ self.student_list.sort()
69
+
70
+ self.setup_class_grades()
71
+ self.fill_class_grades()
72
+ self.get_class_stats()
73
+
74
+
75
+ def setup_class_grades(self):
76
+ """Creates an empty DataFrame to store grades for all students.
77
+
78
+ The DataFrame contains assignment columns and a "Weighted Average Grade" column,
79
+ with students as index labels.
80
+ """
81
+ self.all_student_grades_df = pd.DataFrame(
82
+ np.nan,
83
+ dtype=float,
84
+ index=self.student_list,
85
+ columns=[a.name for a in assignment_type_list] + ["Weighted Average Grade"],
86
+ )
87
+
88
+ def update_student_grade(self, student):
89
+ """Fetches and updates the grade report for an individual student.
90
+
91
+ Args:
92
+ student (str): The username or identifier of the student.
93
+
94
+ Updates:
95
+ The student's row in `all_student_grades_df` with their weighted average grades.
96
+ """
97
+ report = GradeReport(params={"username": student}, display_=False)
98
+ row_series = report.weighted_average_grades.transpose().iloc[
99
+ 0
100
+ ] # Example transformation
101
+ row_series = row_series.reindex(self.all_student_grades_df.columns)
102
+ self.all_student_grades_df.loc[student] = row_series
103
+
104
+ def fill_class_grades(self):
105
+ """Populates the class-wide grade report with data from all students.
106
+
107
+ Iterates through the `student_list` and updates the DataFrame by fetching
108
+ and storing each student's weighted average grades.
109
+ """
110
+ for student in tqdm.tqdm(self.student_list):
111
+ self.update_student_grade(student)
112
+
113
+ def get_class_stats(self):
114
+ """Calculates and stores descriptive statistics for the class-wide grade report.
115
+ Requires filling class grades first
116
+ """
117
+ # Calculate descriptive statistics
118
+ self.stats_df = self.all_student_grades_df.describe(include='all')
119
+
120
+ def write_excel_spreadsheet(self, out_name='output.xlsx'):
121
+ """Exports the class-wide grade report to an Excel spreadsheet.
122
+
123
+ Args:
124
+ out_name (str, optional): Name of output file. Defaults to 'output.xlsx'.
125
+ """
126
+ # Export to Excel with different sheets
127
+ with pd.ExcelWriter('output.xlsx') as writer:
128
+ self.all_student_grades_df.to_excel(writer, sheet_name='all_student_grades')
129
+ self.stats_df.to_excel(writer, sheet_name='performance_statistics')
130
+
131
+
132
+ def main():
133
+ class_grades = ClassGradeReport()
134
+ print(class_grades.all_student_grades_df)
135
+
136
+ if __name__ == "__main__":
137
+ main()
@@ -0,0 +1,353 @@
1
+ # TODO: if not due yet and score is 0, make NAN, fix the rendering
2
+
3
+ from pykubegrader.telemetry import get_assignments_submissions
4
+ from pykubegrader.grade_reports.assignments import Assignment
5
+ from pykubegrader.grade_reports.grading_config import (
6
+ assignment_type_list,
7
+ aliases,
8
+ globally_exempted_assignments,
9
+ dropped_assignments,
10
+ optional_drop_week,
11
+ exclude_from_running_avg,
12
+ custom_grade_adjustments,
13
+ )
14
+
15
+ import pandas as pd
16
+ from datetime import datetime
17
+ from IPython.display import display
18
+ import numpy as np
19
+
20
+ from ..telemetry import get_assignments_submissions
21
+
22
+
23
+ class GradeReport:
24
+ """Class to generate a grade report for a course and perform grade calculations for each student."""
25
+
26
+ def __init__(self, start_date="2025-01-06", verbose=True, params=None, display_ = True):
27
+ """Initializes an instance of the GradeReport class.
28
+ Args:
29
+ start_date (str, optional): The start date of the course. Defaults to "2025-01-06".
30
+ verbose (bool, optional): Indicates if verbose output should be displayed. Defaults to True.
31
+ """
32
+ self.assignments, self.student_subs = get_assignments_submissions(params=params)
33
+
34
+ self.start_date = start_date
35
+ self.verbose = verbose
36
+ self.assignment_type_list = assignment_type_list
37
+ self.aliases = aliases
38
+ self.globally_exempted_assignments = globally_exempted_assignments
39
+ self.dropped_assignments = dropped_assignments
40
+ self.optional_drop_week = optional_drop_week
41
+
42
+ # assignments that have been dropped for a given students.
43
+ self.student_assignments_dropped = []
44
+
45
+ self.setup_grades_df()
46
+ self.build_assignments()
47
+ self.update_global_exempted_assignments()
48
+ self.calculate_grades()
49
+ self.update_assignments_not_due_yet()
50
+ self.calculate_grades()
51
+ self.drop_lowest_n_for_types(1)
52
+ self.update_weekly_table()
53
+ self._build_running_avg()
54
+ self._calculate_final_average()
55
+ if display_:
56
+ try:
57
+ display(self.weekly_grades_df)
58
+ display(self.weighted_average_grades)
59
+ except: # noqa: E722
60
+ pass
61
+
62
+ def update_assignments_not_due_yet(self):
63
+ """
64
+ Updates the score of assignments that are not due yet to NaN.
65
+ """
66
+ for assignment in self.graded_assignments:
67
+ if assignment.due_date:
68
+ # Convert due date to datetime object
69
+ due_date = datetime.fromisoformat(assignment.due_date.replace("Z", "+00:00"))
70
+ if due_date > datetime.now(due_date.tzinfo) and assignment.score == 0:
71
+ assignment.score = np.nan
72
+ assignment.exempted = True
73
+
74
+ def _calculate_final_average(self):
75
+ total_percentage = 1
76
+ df_ = self.compute_final_average()
77
+ score_earned = 0
78
+
79
+ for assignment_type in self.assignment_type_list:
80
+ if assignment_type.name in exclude_from_running_avg:
81
+ total_percentage -= assignment_type.weight
82
+
83
+ score_earned += assignment_type.weight * df_[assignment_type.name]
84
+
85
+ self.final_grade = score_earned / total_percentage
86
+ self.weighted_average_grades = pd.concat(
87
+ [
88
+ pd.DataFrame(self.final_grades),
89
+ pd.DataFrame(
90
+ {"Running Avg": [self.final_grade]},
91
+ index=["Weighted Average Grade"],
92
+ ),
93
+ ]
94
+ )
95
+
96
+ def grade_report(self):
97
+ """Generates a grade report for the course.
98
+ Returns:
99
+ pd.DataFrame: A DataFrame containing the grade report or weekly grades only.
100
+ """
101
+ self._update_running_avg()
102
+ return self.weekly_grades_df
103
+
104
+ def update_weekly_table(self):
105
+ """Updates the weekly grades table with the calculated scores."""
106
+ for assignment in self.graded_assignments:
107
+ if assignment.weekly:
108
+ self.weekly_grades_df.loc[f"week{assignment.week}", assignment.name] = (
109
+ assignment.score
110
+ )
111
+
112
+ def update_global_exempted_assignments(self):
113
+ """Updates the graded assignments with the globally exempted assignments. If assignment doesn't exist, pass."""
114
+ for assignment_type, week in self.globally_exempted_assignments:
115
+ try:
116
+ self.get_graded_assignment(week, assignment_type)[0].exempted = True
117
+ except: # noqa: E722
118
+ pass
119
+
120
+ def build_assignments(self):
121
+ """Generates a list of Assignment objects for each week, applying custom adjustments where needed."""
122
+ self.graded_assignments = []
123
+ weekly_assignments = self.get_weekly_assignments()
124
+
125
+ for assignment_type in weekly_assignments:
126
+ for week in range(1, self.get_num_weeks() + 1): # Weeks start at 1
127
+ self.graded_assignment_constructor(assignment_type, week=week)
128
+
129
+ non_weekly_assignments = self.get_non_weekly_assignments()
130
+
131
+ for assignment_type in non_weekly_assignments:
132
+ self.graded_assignment_constructor(assignment_type)
133
+
134
+ def graded_assignment_constructor(self, assignment_type: str, **kwargs):
135
+ """Constructs a graded assignment object and appends it to the graded_assignments list.
136
+
137
+ Args:
138
+ assignment_type (str): Type of assignment. Options: readings, lecture, practicequiz, quiz, homework, lab, labattendance, practicemidterm, midterm, practicefinal, final.
139
+ """
140
+ custom_func = custom_grade_adjustments.get(
141
+ (assignment_type.name, kwargs.get("week", None)), None
142
+ )
143
+
144
+ filtered_assignments = self.get_assignment(
145
+ kwargs.get("week", None), assignment_type.name
146
+ )
147
+
148
+ new_assignment = Assignment(
149
+ name=assignment_type.name,
150
+ weekly=assignment_type.weekly,
151
+ weight=assignment_type.weight,
152
+ score=0,
153
+ grade_adjustment_func=custom_func,
154
+ # filters the submissions for an assignment and gets the last due date
155
+ due_date=self.determine_due_date(filtered_assignments),
156
+ max_score=self.get_max_score(filtered_assignments),
157
+ **kwargs,
158
+ )
159
+ self.graded_assignments.append(new_assignment)
160
+
161
+ def calculate_grades(self):
162
+ """Calculates the grades for each student based on the graded assignments.
163
+ If there are filtered assignments, the score is updated based on the submission.
164
+ Otherwise,
165
+ """
166
+ for assignment in self.graded_assignments:
167
+ filtered_submission = self.filter_submissions(
168
+ assignment.week, assignment.name
169
+ )
170
+
171
+ if filtered_submission:
172
+ for submission in filtered_submission:
173
+ assignment.update_score(submission)
174
+
175
+ # runs if there are no filtered submissions
176
+ else:
177
+ assignment.update_score()
178
+
179
+ def compute_final_average(self):
180
+ """
181
+ Computes the final average by combining the running average from weekly assignments
182
+ and the midterm/final exam scores.
183
+ """
184
+
185
+ # Extract running average from the weekly table
186
+ self.final_grades = self.weekly_grades_df.loc["Running Avg"]
187
+
188
+ for assignment in self.graded_assignments:
189
+ if not assignment.weekly:
190
+ self.final_grades[f"{assignment.name}"] = assignment.score
191
+
192
+ return self.final_grades
193
+
194
+ def filter_submissions(self, week_number, assignment_type):
195
+ # Normalize the assignment type using aliases
196
+ normalized_type = self.aliases.get(
197
+ assignment_type.lower(), [assignment_type.lower()]
198
+ )
199
+
200
+ if week_number:
201
+ # Filter the assignments based on the week number and normalized assignment type
202
+ filtered = [
203
+ assignment
204
+ for assignment in self.student_subs
205
+ if assignment["week_number"] == week_number
206
+ and assignment["assignment_type"].lower() in normalized_type
207
+ ]
208
+
209
+ # If week_number is None, filter based on the normalized assignment type only
210
+ else:
211
+ # Filter the assignments based on the normalized assignment type
212
+ filtered = [
213
+ assignment
214
+ for assignment in self.student_subs
215
+ if assignment["assignment_type"].lower() in normalized_type
216
+ ]
217
+
218
+ return filtered
219
+
220
+ def get_assignment(self, week_number, assignment_type):
221
+ # Normalize the assignment type using aliases
222
+ normalized_type = self.aliases.get(
223
+ assignment_type.lower(), [assignment_type.lower()]
224
+ )
225
+
226
+ # Filter the assignments based on the week number and normalized assignment type
227
+ filtered = [
228
+ assignment
229
+ for assignment in self.assignments
230
+ if (assignment["week_number"] == week_number or week_number is None)
231
+ and assignment["assignment_type"].lower() in normalized_type
232
+ ]
233
+
234
+ return filtered
235
+
236
+ def get_graded_assignment(self, week_number, assignment_type):
237
+ return list(
238
+ filter(
239
+ lambda a: isinstance(a, Assignment)
240
+ and a.name == assignment_type
241
+ and (week_number is None or a.week == week_number),
242
+ self.graded_assignments,
243
+ )
244
+ )
245
+
246
+ def get_max_score(self, filtered_assignments):
247
+ if not filtered_assignments:
248
+ return None
249
+
250
+ return max(filtered_assignments, key=lambda x: x["id"])["max_score"]
251
+
252
+ def determine_due_date(self, filtered_assignments):
253
+ if not filtered_assignments:
254
+ return None # Return None if the list is empty
255
+
256
+ # Convert due_date strings to datetime objects and find the max
257
+ max_due = max(
258
+ filtered_assignments,
259
+ key=lambda x: datetime.fromisoformat(x["due_date"].replace("Z", "+00:00")),
260
+ )
261
+
262
+ return max_due["due_date"] # Return the max due date as a string
263
+
264
+ def get_non_weekly_assignments(self):
265
+ """Get all weekly assignments from the assignment list configuration"""
266
+ non_weekly_assignments = [
267
+ assignment
268
+ for assignment in self.assignment_type_list
269
+ if not assignment.weekly
270
+ ]
271
+ return non_weekly_assignments
272
+
273
+ def get_weekly_assignments(self):
274
+ """Get all weekly assignments from the assignment list configuration"""
275
+ weekly_assignments = [
276
+ assignment for assignment in self.assignment_type_list if assignment.weekly
277
+ ]
278
+ return weekly_assignments
279
+
280
+ def get_num_weeks(self):
281
+ """Get the number of weeks in the course"""
282
+ max_week_number = max(item["week_number"] for item in self.assignments)
283
+ return max_week_number
284
+
285
+ def setup_grades_df(self):
286
+ weekly_assignments = self.get_weekly_assignments()
287
+ max_week_number = self.get_num_weeks()
288
+ inds = [f"week{i + 1}" for i in range(max_week_number)] + ["Running Avg"]
289
+ restruct_grades = {
290
+ k.name: [0 for i in range(len(inds))] for k in weekly_assignments
291
+ }
292
+ new_weekly_grades = pd.DataFrame(restruct_grades, dtype=float)
293
+ new_weekly_grades["inds"] = inds
294
+ new_weekly_grades.set_index("inds", inplace=True)
295
+ self.weekly_grades_df = new_weekly_grades
296
+
297
+ def _build_running_avg(self):
298
+ """
299
+ Subfunction to compute and update the Running Avg row, handling NaNs.
300
+ """
301
+
302
+ self.weekly_grades_df.loc["Running Avg"] = self.weekly_grades_df.drop(
303
+ "Running Avg", errors="ignore"
304
+ ).mean(axis=0, skipna=True)
305
+
306
+ def drop_lowest_n_for_types(self, n, assignments_=None):
307
+ """
308
+ Exempts the lowest n assignments for each specified assignment type.
309
+ If the lowest dropped score is from week 1, an additional lowest score is dropped.
310
+
311
+ :param assignments_: List of assignment types (names) to process.
312
+ :param n: Number of lowest scores to exempt per type.
313
+ """
314
+ from collections import defaultdict
315
+ import numpy as np
316
+
317
+ # Group assignments by name
318
+ assignment_groups = defaultdict(list)
319
+ for assignment in self.graded_assignments:
320
+ if assignments_ is None:
321
+ if (
322
+ assignment.name in self.dropped_assignments
323
+ and not assignment.exempted
324
+ ):
325
+ assignment_groups[assignment.name].append(assignment)
326
+ else:
327
+ if assignment.name in assignments_ and not assignment.exempted:
328
+ assignment_groups[assignment.name].append(assignment)
329
+
330
+ # Iterate over each specified assignment type and drop the lowest n scores
331
+ for name, assignments in assignment_groups.items():
332
+ # Filter assignments that are not already exempted (NaN scores should not count)
333
+ valid_assignments = [a for a in assignments if not np.isnan(a.score)]
334
+
335
+ # Sort assignments by score in ascending order
336
+ valid_assignments.sort(key=lambda a: a.score)
337
+
338
+ # Exempt the lowest `n` assignments
339
+ dropped = []
340
+ for i in range(min(n, len(valid_assignments))):
341
+ valid_assignments[i].exempted = True
342
+ dropped.append(valid_assignments[i])
343
+ self.student_assignments_dropped.append(valid_assignments[i])
344
+
345
+ # Check if the lowest dropped assignment is from week 1
346
+ if dropped and any(a.week in self.optional_drop_week for a in dropped):
347
+ # Find the next lowest non-exempted assignment and drop it
348
+ for a in valid_assignments:
349
+ if not a.exempted:
350
+ a.exempted = True
351
+ break
352
+
353
+ self.calculate_grades()
@@ -0,0 +1,70 @@
1
+ from pykubegrader.grade_reports.assignments import assignment_type
2
+
3
+ # Assignment types, weekly, and weight
4
+ # Total should add up to 1.0
5
+ assignment_type_list = [
6
+ assignment_type("readings", True, 0.15),
7
+ assignment_type("lecture", True, 0.15),
8
+ assignment_type("practicequiz", True, 0.015),
9
+ assignment_type("quiz", True, 0.15),
10
+ assignment_type("homework", True, 0.15),
11
+ assignment_type("lab", True, 0.15),
12
+ assignment_type("labattendance", True, 0.05),
13
+ assignment_type("practicemidterm", False, 0.015),
14
+ assignment_type("midterm", False, 0.15),
15
+ assignment_type("practicefinal", False, 0.02),
16
+ assignment_type("final", False, 0.2),
17
+ ]
18
+
19
+ # Custom grade adjustments, key is a tuple of assignment type and week, value is a lambda function that takes the score and returns the adjusted score
20
+ custom_grade_adjustments = {
21
+ ("lecture", 3): lambda score: 100.0 if score > 0 else 0.0,
22
+ ("lecture", 4): lambda score: 100.0 if score > 0 else 0.0,
23
+ ("lecture", 5): lambda score: 100.0 if score > 0 else 0.0,
24
+ }
25
+
26
+ # Exempted assignments, key is a tuple of assignment type and week
27
+ globally_exempted_assignments = [
28
+ ("readings", 6),
29
+ ("quiz", 6),
30
+ ("practicequiz", 6),
31
+ ("lecture", 6),
32
+ ("homework", 5),
33
+ ("lab", 5),
34
+ ("labattendance", 5),
35
+ ]
36
+
37
+ # Common Assignment Aliases, these are other names used for the same assignment category
38
+ aliases = {
39
+ "practicequiz": [
40
+ "practice quiz",
41
+ "practice-quiz",
42
+ "practice quiz",
43
+ "practice_quiz",
44
+ "practicequiz",
45
+ ],
46
+ "labattendance": ["labattendance", "attendance", "attend"],
47
+ }
48
+
49
+ # Skipped assignments, key is a tuple of assignment type and week
50
+ skipped_assignments = {}
51
+
52
+ # Dropped assignments a list of assignments which lowest score will be dropped
53
+ dropped_assignments = [
54
+ "readings",
55
+ "lecture",
56
+ "practicequiz",
57
+ "quiz",
58
+ "homework",
59
+ "lab",
60
+ "labattendance",
61
+ ]
62
+
63
+ # TAs and other users to skip in class grade report
64
+ skipped_users = ['JCA', 'jca92', 'cnp68', 'dak329', 'xz498', 'ag4328', 'rg897', 'jce63', 'qt49']
65
+
66
+ # Optional drop week, a list of weeks where the lowest score will be dropped
67
+ optional_drop_week = [1]
68
+
69
+ # Excluded from running average, a list of assignment types that will be excluded from the running average calculation
70
+ exclude_from_running_avg = ["final"]
@@ -0,0 +1,43 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from pykubegrader.grade_reports.class_grade_report import ClassGradeReport\n",
10
+ "\n",
11
+ "out = ClassGradeReport().run()"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": null,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": []
20
+ }
21
+ ],
22
+ "metadata": {
23
+ "kernelspec": {
24
+ "display_name": "engr131_dev",
25
+ "language": "python",
26
+ "name": "python3"
27
+ },
28
+ "language_info": {
29
+ "codemirror_mode": {
30
+ "name": "ipython",
31
+ "version": 3
32
+ },
33
+ "file_extension": ".py",
34
+ "mimetype": "text/x-python",
35
+ "name": "python",
36
+ "nbconvert_exporter": "python",
37
+ "pygments_lexer": "ipython3",
38
+ "version": "3.12.7"
39
+ }
40
+ },
41
+ "nbformat": 4,
42
+ "nbformat_minor": 2
43
+ }
@@ -1,5 +1,56 @@
1
1
  {
2
2
  "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "from datetime import datetime\n",
10
+ "\n",
11
+ "import pandas as pd\n",
12
+ "import requests\n",
13
+ "from build.passwords import password, user\n",
14
+ "from grade_reports.grade_reports import filter_assignments, get_student_grades\n",
15
+ "from requests.auth import HTTPBasicAuth\n",
16
+ "\n",
17
+ "from pykubegrader.graders.late_assignments import calculate_late_submission\n",
18
+ "\n",
19
+ "api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\" "
20
+ ]
21
+ },
22
+ {
23
+ "cell_type": "code",
24
+ "execution_count": null,
25
+ "metadata": {},
26
+ "outputs": [],
27
+ "source": [
28
+ "from telemetry import get_assignments_submissions\n"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "execution_count": null,
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "self.assignments, self.student_subs = get_assignments_submissions()"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "metadata": {},
44
+ "outputs": [],
45
+ "source": []
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "metadata": {},
51
+ "outputs": [],
52
+ "source": []
53
+ },
3
54
  {
4
55
  "cell_type": "code",
5
56
  "execution_count": null,
@@ -466,7 +517,7 @@
466
517
  ],
467
518
  "metadata": {
468
519
  "kernelspec": {
469
- "display_name": "engr131w25",
520
+ "display_name": "engr131_dev",
470
521
  "language": "python",
471
522
  "name": "python3"
472
523
  },
@@ -480,7 +531,7 @@
480
531
  "name": "python",
481
532
  "nbconvert_exporter": "python",
482
533
  "pygments_lexer": "ipython3",
483
- "version": "3.13.1"
534
+ "version": "3.12.7"
484
535
  }
485
536
  },
486
537
  "nbformat": 4,
@@ -42,9 +42,10 @@ 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
+
46
+ token = os.getenv("TOKEN")
47
+ if token:
48
+ params["key_used"] = token
48
49
 
49
50
  username, password = get_credentials().values()
50
51