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.
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/METADATA +2 -1
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/RECORD +22 -16
- pykubegrader/build/api_notebook_builder.py +43 -27
- pykubegrader/build/build_folder.py +29 -15
- pykubegrader/grade_reports/assignments.py +184 -0
- pykubegrader/grade_reports/class_grade_report.py +137 -0
- pykubegrader/grade_reports/grade_report.py +353 -0
- pykubegrader/grade_reports/grading_config.py +70 -0
- pykubegrader/grade_reports/test.ipynb +43 -0
- pykubegrader/grading_tester.ipynb +53 -2
- pykubegrader/submit/submit_assignment.py +4 -3
- pykubegrader/telemetry.py +88 -84
- pykubegrader/tokens/generator.py +75 -0
- pykubegrader/tokens/token_panel.py +2 -2
- pykubegrader/tokens/tokens.py +0 -1
- pykubegrader/widgets_base/multi_select.py +1 -1
- pykubegrader/widgets_base/select.py +1 -1
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/LICENSE.txt +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/WHEEL +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/entry_points.txt +0 -0
- {PyKubeGrader-0.3.7.dist-info → PyKubeGrader-0.3.9.dist-info}/top_level.txt +0 -0
- /pykubegrader/grade_reports/{grade_reports.py → __grade_reports.py} +0 -0
pykubegrader/telemetry.py
CHANGED
@@ -16,8 +16,8 @@ from requests import Response
|
|
16
16
|
from requests.auth import HTTPBasicAuth
|
17
17
|
from requests.exceptions import RequestException
|
18
18
|
|
19
|
-
from .graders.late_assignments import calculate_late_submission
|
20
|
-
from .utils import api_base_url, student_pw, student_user
|
19
|
+
from pykubegrader.graders.late_assignments import calculate_late_submission
|
20
|
+
from pykubegrader.utils import api_base_url, student_pw, student_user
|
21
21
|
|
22
22
|
#
|
23
23
|
# Logging setup
|
@@ -291,26 +291,63 @@ def upload_execution_log() -> None:
|
|
291
291
|
print("Execution log uploaded successfully")
|
292
292
|
|
293
293
|
|
294
|
-
|
295
|
-
|
296
|
-
|
294
|
+
def get_all_students(user, password):
|
295
|
+
"""
|
296
|
+
Fetches a list of all students from the API and returns their usernames.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
user (str): The username for HTTP basic authentication.
|
300
|
+
password (str): The password for HTTP basic authentication.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
list: A list of usernames extracted from the students' email addresses.
|
304
|
+
|
305
|
+
Raises:
|
306
|
+
requests.exceptions.HTTPError: If the HTTP request returned an unsuccessful status code.
|
307
|
+
"""
|
308
|
+
res = requests.get(
|
309
|
+
url=api_base_url.rstrip("/") + "/students",
|
310
|
+
auth=HTTPBasicAuth(user, password),
|
311
|
+
)
|
312
|
+
res.raise_for_status()
|
313
|
+
|
314
|
+
# Input: List of players
|
315
|
+
return [student["email"].split("@")[0] for student in res.json()]
|
297
316
|
|
298
317
|
|
299
|
-
def get_assignments_submissions():
|
318
|
+
def get_assignments_submissions(params=None):
|
319
|
+
"""
|
320
|
+
Fetches assignment submissions for a student from the grading API.
|
321
|
+
This function retrieves the assignment submissions for a student by making a GET request to the grading API.
|
322
|
+
It requires certain environment variables to be set and validates the JupyterHub username.
|
323
|
+
Args:
|
324
|
+
params (dict, optional): A dictionary of parameters to be sent in the query string. Defaults to None. If not provided, it will default to {"username": <JUPYTERHUB_USER>}.
|
325
|
+
Raises:
|
326
|
+
ValueError: If necessary environment variables (student_user, student_pw, api_base_url) are not set.
|
327
|
+
ValueError: If there is a mismatch between the JupyterHub username from the hostname and the environment variable.
|
328
|
+
Returns:
|
329
|
+
dict: A dictionary containing the JSON response from the API with the assignment submissions.
|
330
|
+
"""
|
331
|
+
|
300
332
|
if not student_user or not student_pw or not api_base_url:
|
301
333
|
raise ValueError("Necessary environment variables not set")
|
334
|
+
|
302
335
|
from_hostname = socket.gethostname().removeprefix("jupyter-")
|
303
336
|
from_env = os.getenv("JUPYTERHUB_USER")
|
337
|
+
|
304
338
|
if from_hostname != from_env:
|
305
339
|
raise ValueError("Problem with JupyterHub username")
|
306
|
-
|
307
|
-
|
340
|
+
|
341
|
+
if not params:
|
342
|
+
params = {"username": from_env}
|
343
|
+
|
308
344
|
# get submission information
|
309
345
|
res = requests.get(
|
310
346
|
url=api_base_url.rstrip("/") + "/my-grades-testing",
|
311
347
|
params=params,
|
312
348
|
auth=HTTPBasicAuth(student_user, student_pw),
|
313
349
|
)
|
350
|
+
|
314
351
|
return res.json()
|
315
352
|
|
316
353
|
|
@@ -319,7 +356,7 @@ def setup_grades_df(assignments):
|
|
319
356
|
|
320
357
|
inds = [f"week{i + 1}" for i in range(11)] + ["Running Avg"]
|
321
358
|
restruct_grades = {k: [0 for i in range(len(inds))] for k in assignment_types}
|
322
|
-
new_weekly_grades = pd.DataFrame(restruct_grades,dtype=float)
|
359
|
+
new_weekly_grades = pd.DataFrame(restruct_grades, dtype=float)
|
323
360
|
new_weekly_grades["inds"] = inds
|
324
361
|
new_weekly_grades.set_index("inds", inplace=True)
|
325
362
|
return new_weekly_grades
|
@@ -329,19 +366,31 @@ def skipped_assignment_mask(assignments):
|
|
329
366
|
existing_assignment_mask = setup_grades_df(assignments).astype(bool)
|
330
367
|
for assignment in assignments:
|
331
368
|
# existing_assignment_mask[assignment["assignment_type"]].iloc[assignment["week_number"]-1] = True
|
332
|
-
existing_assignment_mask.loc[
|
369
|
+
existing_assignment_mask.loc[
|
370
|
+
f"week{assignment['week_number']}", assignment["assignment_type"]
|
371
|
+
] = True
|
333
372
|
return existing_assignment_mask.astype(bool)
|
334
373
|
|
374
|
+
|
335
375
|
def fill_grades_df(new_weekly_grades, assignments, student_subs):
|
336
376
|
for assignment in assignments:
|
337
377
|
# get the assignment from all submissions
|
338
|
-
subs = [
|
378
|
+
subs = [
|
379
|
+
sub
|
380
|
+
for sub in student_subs
|
381
|
+
if (sub["assignment_type"] == assignment["assignment_type"])
|
382
|
+
and (sub["week_number"] == assignment["week_number"])
|
383
|
+
]
|
339
384
|
# print(assignment, subs)
|
340
385
|
# print(assignment)
|
341
386
|
# print(student_subs[:5])
|
342
387
|
if assignment["assignment_type"] == "lecture":
|
343
|
-
if
|
344
|
-
|
388
|
+
if (
|
389
|
+
sum([sub["raw_score"] for sub in subs]) > 0
|
390
|
+
): # TODO: good way to check for completion?
|
391
|
+
new_weekly_grades.loc[f"week{assignment['week_number']}", "lecture"] = (
|
392
|
+
1.0
|
393
|
+
)
|
345
394
|
if assignment["assignment_type"] == "final":
|
346
395
|
continue
|
347
396
|
if assignment["assignment_type"] == "midterm":
|
@@ -370,15 +419,17 @@ def fill_grades_df(new_weekly_grades, assignments, student_subs):
|
|
370
419
|
# print(assignment['title'], grades, assignment['max_score'])
|
371
420
|
grade = max(grades) / assignment["max_score"]
|
372
421
|
|
373
|
-
# fill out new df with max
|
374
|
-
new_weekly_grades.loc[
|
375
|
-
f"week{assignment['week_number']}", assignment["assignment_type"]
|
376
|
-
] = grade
|
377
422
|
|
378
423
|
# Merge different names
|
379
|
-
new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(
|
380
|
-
|
381
|
-
|
424
|
+
new_weekly_grades["attend"] = new_weekly_grades[["attend", "attendance"]].max(
|
425
|
+
axis=1
|
426
|
+
)
|
427
|
+
new_weekly_grades["practicequiz"] = new_weekly_grades[
|
428
|
+
["practicequiz", "practice-quiz"]
|
429
|
+
].max(axis=1)
|
430
|
+
new_weekly_grades["practicemidterm"] = new_weekly_grades[
|
431
|
+
["practicemidterm", "PracticeMidterm"]
|
432
|
+
].max(axis=1)
|
382
433
|
new_weekly_grades.drop(
|
383
434
|
["attendance", "practice-quiz", "test", "PracticeMidterm"],
|
384
435
|
axis=1,
|
@@ -386,7 +437,8 @@ def fill_grades_df(new_weekly_grades, assignments, student_subs):
|
|
386
437
|
errors="ignore",
|
387
438
|
)
|
388
439
|
|
389
|
-
|
440
|
+
# return new_weekly_grades
|
441
|
+
|
390
442
|
|
391
443
|
def get_current_week(start_date):
|
392
444
|
# Calculate the current week (1-based indexing)
|
@@ -396,11 +448,13 @@ def get_current_week(start_date):
|
|
396
448
|
return days_since_start // 7 + 1
|
397
449
|
|
398
450
|
|
399
|
-
def get_average_weighted_grade(assignments,
|
451
|
+
def get_average_weighted_grade(assignments, new_weekly_grades, weights):
|
400
452
|
# Get average until current week
|
401
453
|
skip_weeks = skipped_assignment_mask(assignments)
|
402
454
|
for col in new_weekly_grades.columns:
|
403
|
-
new_weekly_grades.loc["Running Avg", col] = new_weekly_grades.loc[
|
455
|
+
new_weekly_grades.loc["Running Avg", col] = new_weekly_grades.loc[
|
456
|
+
skip_weeks[col], col
|
457
|
+
].mean()
|
404
458
|
# for col in new_weekly_grades.columns:
|
405
459
|
# skip_weeks = skipped_assignment_mask(assignments)
|
406
460
|
# skip_weeks_series = pd.Series(skip_weeks)
|
@@ -414,8 +468,8 @@ def get_average_weighted_grade(assignments, current_week, new_weekly_grades, wei
|
|
414
468
|
grade = new_weekly_grades.get(k, pd.Series([0])).iloc[-1]
|
415
469
|
total += grade * v
|
416
470
|
avg_grades_dict[k] = grade
|
417
|
-
avg_grades_dict[
|
418
|
-
|
471
|
+
avg_grades_dict["Total"] = total # excluded midterm and final
|
472
|
+
|
419
473
|
return avg_grades_dict
|
420
474
|
|
421
475
|
|
@@ -425,7 +479,7 @@ def get_my_grades_testing(start_date="2025-01-06", verbose=True):
|
|
425
479
|
reshapes columns into reading, lecture, practicequiz, quiz, lab, attendance, homework, exam, final.
|
426
480
|
fills in 0 for missing assignments
|
427
481
|
calculate running average of each category"""
|
428
|
-
|
482
|
+
|
429
483
|
# set up new df format
|
430
484
|
weights = {
|
431
485
|
"homework": 0.15,
|
@@ -439,71 +493,21 @@ def get_my_grades_testing(start_date="2025-01-06", verbose=True):
|
|
439
493
|
}
|
440
494
|
|
441
495
|
assignments, student_subs = get_assignments_submissions()
|
442
|
-
|
496
|
+
|
443
497
|
new_grades_df = setup_grades_df(assignments)
|
444
498
|
|
445
499
|
new_weekly_grades = fill_grades_df(new_grades_df, assignments, student_subs)
|
446
500
|
|
447
|
-
current_week = get_current_week(start_date)
|
448
|
-
|
449
|
-
avg_grades_dict = get_average_weighted_grade(
|
450
|
-
|
501
|
+
# current_week = get_current_week(start_date)
|
502
|
+
|
503
|
+
avg_grades_dict = get_average_weighted_grade(
|
504
|
+
assignments, new_weekly_grades, weights
|
505
|
+
)
|
506
|
+
|
451
507
|
if verbose:
|
452
508
|
max_key_length = max(len(k) for k in weights.keys())
|
453
509
|
for k, v in avg_grades_dict.items():
|
454
|
-
print(f
|
510
|
+
print(f"{k:<{max_key_length}}:\t {v:.2f}")
|
455
511
|
|
456
512
|
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()
|
463
|
-
|
464
|
-
# Input: List of players
|
465
|
-
return [student['email'].split('@')[0] for student in res.json()]
|
466
|
-
|
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
513
|
|
508
|
-
# return new_weekly_grades # get rid of test and running avg columns
|
509
|
-
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
import panel as pn
|
4
|
+
import requests
|
5
|
+
from panel.pane import Markdown
|
6
|
+
from panel.widgets import Button, Select, TextInput
|
7
|
+
from requests.auth import HTTPBasicAuth
|
8
|
+
|
9
|
+
from ..utils import api_base_url, student_pw, student_user
|
10
|
+
|
11
|
+
pn.extension()
|
12
|
+
|
13
|
+
|
14
|
+
class TokenGenerator:
|
15
|
+
def __init__(self) -> None:
|
16
|
+
if not student_user or not student_pw:
|
17
|
+
raise RuntimeError("API credentials not found in environment variables")
|
18
|
+
|
19
|
+
self.username = student_user
|
20
|
+
self.password = student_pw
|
21
|
+
|
22
|
+
self.students: list[str] = self.fetch_options("students", key="email")
|
23
|
+
self.assignments: list[str] = self.fetch_options("assignments", key="title")
|
24
|
+
|
25
|
+
# Empty lists are falsy
|
26
|
+
if not self.students or not self.assignments:
|
27
|
+
raise RuntimeError("No students found, or no assignments")
|
28
|
+
|
29
|
+
self.stud_select = Select(name="Student email", options=self.students)
|
30
|
+
self.assn_select = Select(name="Assignment name", options=self.assignments)
|
31
|
+
self.token_input = TextInput(name="Desired token value")
|
32
|
+
self.submit_btn = Button(name="Submit", button_type="primary")
|
33
|
+
self.status_msg = Markdown("")
|
34
|
+
|
35
|
+
self.submit_btn.on_click(self.request_token)
|
36
|
+
|
37
|
+
def fetch_options(self, endpoint: str, key: str) -> list[str]:
|
38
|
+
try:
|
39
|
+
response = requests.get(
|
40
|
+
url=f"{api_base_url}/{endpoint}",
|
41
|
+
auth=HTTPBasicAuth(self.username, self.password),
|
42
|
+
)
|
43
|
+
response.raise_for_status()
|
44
|
+
return sorted([item[key] for item in response.json()])
|
45
|
+
except requests.exceptions.RequestException as e:
|
46
|
+
print(f"Error fetching {endpoint}: {e}", file=sys.stderr)
|
47
|
+
return []
|
48
|
+
|
49
|
+
def request_token(self, _) -> None:
|
50
|
+
payload = {
|
51
|
+
"student_id": self.stud_select.value,
|
52
|
+
"assignment": self.assn_select.value,
|
53
|
+
"value": self.token_input.value,
|
54
|
+
}
|
55
|
+
|
56
|
+
try:
|
57
|
+
response = requests.post(
|
58
|
+
url=f"{api_base_url}/tokens",
|
59
|
+
auth=HTTPBasicAuth(self.username, self.password),
|
60
|
+
json=payload,
|
61
|
+
)
|
62
|
+
response.raise_for_status()
|
63
|
+
self.status_msg.object = "✅ Token generated successfully!"
|
64
|
+
except requests.exceptions.RequestException as e:
|
65
|
+
self.status_msg.object = f"❌ Submission failed: {e}"
|
66
|
+
|
67
|
+
def show(self) -> pn.Column:
|
68
|
+
return pn.Column(
|
69
|
+
"# Generate Scoped Token",
|
70
|
+
self.stud_select,
|
71
|
+
self.assn_select,
|
72
|
+
self.token_input,
|
73
|
+
self.submit_btn,
|
74
|
+
self.status_msg,
|
75
|
+
)
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import os
|
2
|
+
|
1
3
|
import panel as pn
|
2
4
|
import requests
|
3
5
|
from requests.auth import HTTPBasicAuth
|
4
|
-
import os
|
5
6
|
|
6
7
|
from ..utils import api_base_url
|
7
8
|
|
@@ -23,7 +24,6 @@ def get_jhub_user():
|
|
23
24
|
|
24
25
|
|
25
26
|
def get_students():
|
26
|
-
|
27
27
|
# Make the request
|
28
28
|
response = requests.get(
|
29
29
|
f"{api_base_url}students",
|
pykubegrader/tokens/tokens.py
CHANGED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|