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/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
- # # Qiao's work on grades
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
- print(from_env)
307
- params = {"username": from_env}
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[f'week{assignment["week_number"]}', assignment["assignment_type"]] = True
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 = [ sub for sub in student_subs if (sub['assignment_type']==assignment['assignment_type']) and (sub['week_number']==assignment['week_number']) ]
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 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
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(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)
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
- return new_weekly_grades
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, current_week, new_weekly_grades, weights):
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[skip_weeks[col]==True, col].mean()
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['Total'] = total # excluded midterm and final
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(assignments, current_week, new_weekly_grades, weights)
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'{k:<{max_key_length}}:\t {v:.2f}')
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",
@@ -7,7 +7,6 @@ from ..utils import api_base_url
7
7
 
8
8
 
9
9
  def build_token_payload(token: str, duration: int, **kwargs) -> dict:
10
-
11
10
  student_id = kwargs.get("student_id", None)
12
11
  assignment = kwargs.get("assignment", None)
13
12
 
@@ -1,8 +1,8 @@
1
1
  import time
2
2
  from typing import Callable, Optional, Tuple
3
3
 
4
- import panel as pn
5
4
  import numpy as np
5
+ import panel as pn
6
6
 
7
7
  from ..telemetry import ensure_responses, score_question, update_responses
8
8
  from ..utils import shuffle_options, shuffle_questions
@@ -1,8 +1,8 @@
1
1
  import time
2
2
  from typing import Callable, Optional, Tuple
3
3
 
4
- import panel as pn
5
4
  import numpy as np
5
+ import panel as pn
6
6
 
7
7
  from ..telemetry import ensure_responses, score_question, update_responses
8
8
  from ..utils import shuffle_options, shuffle_questions