aarg-canvas 0.1.0__tar.gz
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.
- aarg_canvas-0.1.0/LICENSE +21 -0
- aarg_canvas-0.1.0/PKG-INFO +25 -0
- aarg_canvas-0.1.0/README.md +5 -0
- aarg_canvas-0.1.0/pyproject.toml +26 -0
- aarg_canvas-0.1.0/src/aarg/__init__.py +3 -0
- aarg_canvas-0.1.0/src/aarg/aarg_logging.py +3 -0
- aarg_canvas-0.1.0/src/aarg/canvas_utils.py +30 -0
- aarg_canvas-0.1.0/src/aarg/common_jobs/__init__.py +0 -0
- aarg_canvas-0.1.0/src/aarg/common_jobs/agent_grading.py +208 -0
- aarg_canvas-0.1.0/src/aarg/common_jobs/zero_grades.py +137 -0
- aarg_canvas-0.1.0/src/aarg/grader.py +49 -0
- aarg_canvas-0.1.0/src/aarg/models.py +123 -0
- aarg_canvas-0.1.0/src/aarg/server.py +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BYU CS Course Ops
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aarg-canvas
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-assisted Rapid Grading for Canvas
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Gordon Bean
|
|
7
|
+
Author-email: gbean@cs.byu.edu
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: canvasapi (>=3.6.0,<4.0.0)
|
|
16
|
+
Requires-Dist: openai (>=2.38.0,<3.0.0)
|
|
17
|
+
Requires-Dist: pydantic (>=2.13.4,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# AARG - Agent-assisted Rapid Grading
|
|
21
|
+
|
|
22
|
+
A python library for building AI-integrated
|
|
23
|
+
autograders, with specific integration with Canvas.
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "aarg-canvas"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Agent-assisted Rapid Grading for Canvas"
|
|
5
|
+
authors = ["Gordon Bean <gbean@cs.byu.edu>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [{ include = "aarg", from = "src" }]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
canvasapi = "^3.6.0"
|
|
13
|
+
openai = "^2.38.0"
|
|
14
|
+
pydantic = "^2.13.4"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pytest = "^9.0.2"
|
|
18
|
+
poetry-version-from-file = "^1.1.0"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.plugins.version-from-file]
|
|
21
|
+
enabled = "true"
|
|
22
|
+
file = "src/VERSION"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core>=1.0.0", "poetry-version-from-file"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from canvasapi.course import Course
|
|
4
|
+
from canvasapi.quiz import Quiz, QuizSubmission
|
|
5
|
+
from canvasapi.submission import Submission
|
|
6
|
+
|
|
7
|
+
QUESTION_ID = int
|
|
8
|
+
QUESTION_TEXT = str
|
|
9
|
+
QUESTION_RESPONSE = str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_quiz_questions(quiz: Quiz, latest_submission: QuizSubmission) -> list[tuple[QUESTION_ID, QUESTION_TEXT, QUESTION_RESPONSE]]:
|
|
13
|
+
"""Return ``(question_id, question_text, student_response)`` tuples.
|
|
14
|
+
We assume that this submission object includes the submission data.
|
|
15
|
+
"""
|
|
16
|
+
# TODO - validate that submission_data is present
|
|
17
|
+
question_responses = {
|
|
18
|
+
q['question_id']: q['text']
|
|
19
|
+
for q in latest_submission['submission_data']
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
question_texts = {
|
|
23
|
+
q.id: q.question_text
|
|
24
|
+
for q in quiz.get_questions()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
(quid, text, question_responses[quid])
|
|
29
|
+
for quid, text in question_texts.items()
|
|
30
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TypedDict
|
|
8
|
+
|
|
9
|
+
from canvasapi.course import Course
|
|
10
|
+
from canvasapi.submission import Submission
|
|
11
|
+
from openai import Client
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from ..aarg_logging import logger
|
|
15
|
+
from ..canvas_utils import get_quiz_questions
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RubricRequirement(TypedDict):
|
|
19
|
+
requirement: str
|
|
20
|
+
"""A true/false assertion about the question"""
|
|
21
|
+
points: float
|
|
22
|
+
"""The points awarded for a true assertion"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QuestionRubric(TypedDict):
|
|
26
|
+
question_text_fragment: str
|
|
27
|
+
requirements: list[RubricRequirement]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AgentRubricRequirementAssessment(BaseModel):
|
|
31
|
+
reasoning: str
|
|
32
|
+
meets_requirement: bool
|
|
33
|
+
feedback: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RubricAgentGrader:
|
|
37
|
+
def __init__(
|
|
38
|
+
self, course: Course, client: Client,
|
|
39
|
+
agent_model: str, agent_prompt: str,
|
|
40
|
+
feedback_prompt: str,
|
|
41
|
+
assignment_name: str,
|
|
42
|
+
rubric: list[QuestionRubric]
|
|
43
|
+
):
|
|
44
|
+
self._course = course
|
|
45
|
+
self._client = client
|
|
46
|
+
self._agent_model = agent_model
|
|
47
|
+
self._agent_prompt = agent_prompt
|
|
48
|
+
self._feedback_prompt = feedback_prompt
|
|
49
|
+
self._assignment_name = assignment_name
|
|
50
|
+
self._rubric = rubric
|
|
51
|
+
|
|
52
|
+
def __call__(self, submission: Submission):
|
|
53
|
+
if self._assignment_name in submission.assignment['name']:
|
|
54
|
+
self._grade_submission(submission)
|
|
55
|
+
|
|
56
|
+
def _grade_submission(self, submission: Submission):
|
|
57
|
+
logger.debug('Submission: %s, %s', submission.id, submission.assignment['name'])
|
|
58
|
+
grades = {}
|
|
59
|
+
|
|
60
|
+
# TODO - validate that submission_history is present
|
|
61
|
+
latest_submission = max(
|
|
62
|
+
submission.submission_history,
|
|
63
|
+
key=lambda s: s['submitted_at']
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
quiz = self._course.get_quiz(-submission.grader_id)
|
|
67
|
+
|
|
68
|
+
for quid, text, student_response in get_quiz_questions(quiz, latest_submission):
|
|
69
|
+
qrubric = self._select_question_rubric(text)
|
|
70
|
+
logger.debug('Quiz: %s Rubric: %s Response: %s Question: %s', quid, qrubric, student_response, text)
|
|
71
|
+
|
|
72
|
+
score, comment = self._grade_questions(qrubric, text, student_response)
|
|
73
|
+
logger.debug('Score: %s Comment: %s', score, comment)
|
|
74
|
+
|
|
75
|
+
grades[quid] = {'score': score, 'comment': comment}
|
|
76
|
+
|
|
77
|
+
quiz_sub = quiz.get_quiz_submission(latest_submission['id'])
|
|
78
|
+
|
|
79
|
+
quiz_sub.update_score_and_comments(
|
|
80
|
+
quiz_submissions=[{
|
|
81
|
+
'attempt': latest_submission['attempt'],
|
|
82
|
+
'questions': grades
|
|
83
|
+
}]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _select_question_rubric(self, text: str) -> QuestionRubric:
|
|
87
|
+
for question_rubric in self._rubric:
|
|
88
|
+
if question_rubric['question_text_fragment'] in text:
|
|
89
|
+
return question_rubric
|
|
90
|
+
raise ValueError(f'No matching rubric for text "{text}"')
|
|
91
|
+
|
|
92
|
+
def _grade_questions(self, qrubric: QuestionRubric, question_text: str, student_response: str) -> tuple[float, str]:
|
|
93
|
+
total_score = 0
|
|
94
|
+
results = []
|
|
95
|
+
for requirement in qrubric['requirements']:
|
|
96
|
+
assessment = self._grade_requirement(question_text, requirement, student_response)
|
|
97
|
+
results.append(assessment)
|
|
98
|
+
|
|
99
|
+
if assessment.meets_requirement:
|
|
100
|
+
total_score += requirement['points']
|
|
101
|
+
|
|
102
|
+
if any(not result.meets_requirement for result in results):
|
|
103
|
+
comments = self._compose_comments(question_text, student_response, results)
|
|
104
|
+
else:
|
|
105
|
+
comments = None
|
|
106
|
+
return total_score, comments
|
|
107
|
+
|
|
108
|
+
def _grade_requirement(
|
|
109
|
+
self,
|
|
110
|
+
question_text: str,
|
|
111
|
+
requirement: RubricRequirement,
|
|
112
|
+
student_response: str
|
|
113
|
+
) -> AgentRubricRequirementAssessment:
|
|
114
|
+
response = self._client.responses.parse(
|
|
115
|
+
model=self._agent_model,
|
|
116
|
+
input=self._agent_prompt.format(
|
|
117
|
+
question_text=question_text,
|
|
118
|
+
requirement=requirement['requirement'],
|
|
119
|
+
student_response=student_response
|
|
120
|
+
),
|
|
121
|
+
text_format=AgentRubricRequirementAssessment
|
|
122
|
+
)
|
|
123
|
+
return response.output_parsed
|
|
124
|
+
|
|
125
|
+
def _compose_comments(
|
|
126
|
+
self,
|
|
127
|
+
question_text: str,
|
|
128
|
+
student_response: str,
|
|
129
|
+
results: list[AgentRubricRequirementAssessment]
|
|
130
|
+
) -> str:
|
|
131
|
+
response = self._client.responses.create(
|
|
132
|
+
model=self._agent_model,
|
|
133
|
+
input=self._feedback_prompt.format(
|
|
134
|
+
question_text=question_text,
|
|
135
|
+
student_response=student_response,
|
|
136
|
+
results=results
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
return response.output_text
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def _main():
|
|
143
|
+
from canvasapi import Canvas
|
|
144
|
+
import os
|
|
145
|
+
course_id = ...
|
|
146
|
+
|
|
147
|
+
canvas = Canvas('https://byu.instructure.com', os.getenv('CANVAS_API_TOKEN'))
|
|
148
|
+
course = canvas.get_course(course_id)
|
|
149
|
+
|
|
150
|
+
client = Client()
|
|
151
|
+
|
|
152
|
+
q1_grader = RubricAgentGrader(
|
|
153
|
+
course,
|
|
154
|
+
client,
|
|
155
|
+
'gpt-5.4-mini',
|
|
156
|
+
Path().joinpath('scratch', 'rubric-grader-prompt.md').read_text(),
|
|
157
|
+
Path().joinpath('scratch', 'feedback-prompt.md').read_text(),
|
|
158
|
+
'1a Interview Questions',
|
|
159
|
+
[
|
|
160
|
+
QuestionRubric(
|
|
161
|
+
question_text_fragment='What is a large language model? What is the fundamental manner in which they work?',
|
|
162
|
+
requirements=[
|
|
163
|
+
RubricRequirement(requirement='Explains that these are trained on massive amounts of data',
|
|
164
|
+
points=1),
|
|
165
|
+
RubricRequirement(requirement='Explains that these predict the next token or word', score=1)
|
|
166
|
+
]
|
|
167
|
+
),
|
|
168
|
+
QuestionRubric(
|
|
169
|
+
question_text_fragment='What are a few tradeoffs of using LLMs with more or fewer parameters?',
|
|
170
|
+
requirements=[
|
|
171
|
+
RubricRequirement(requirement='Describes at least one advantage for LLMs with more parameters',
|
|
172
|
+
points=1),
|
|
173
|
+
RubricRequirement(requirement='Describes at least one advantage for LLMs with fewer parameters',
|
|
174
|
+
points=1)
|
|
175
|
+
]
|
|
176
|
+
),
|
|
177
|
+
QuestionRubric(
|
|
178
|
+
question_text_fragment='What is a prompt?',
|
|
179
|
+
requirements=[
|
|
180
|
+
RubricRequirement(
|
|
181
|
+
requirement='Explains it is the text given to an LLM from which new text is generated',
|
|
182
|
+
points=1),
|
|
183
|
+
]
|
|
184
|
+
),
|
|
185
|
+
QuestionRubric(
|
|
186
|
+
question_text_fragment='What is a token?',
|
|
187
|
+
requirements=[
|
|
188
|
+
RubricRequirement(
|
|
189
|
+
requirement='Explains it is the basic unit of data read and written by an LLM',
|
|
190
|
+
points=1),
|
|
191
|
+
]
|
|
192
|
+
)
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
logging.basicConfig(
|
|
197
|
+
level=logging.WARN,
|
|
198
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
|
199
|
+
)
|
|
200
|
+
logger.setLevel(logging.DEBUG)
|
|
201
|
+
|
|
202
|
+
from ..grader import Grader
|
|
203
|
+
grader = Grader(course, [q1_grader])
|
|
204
|
+
await grader()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
asyncio.run(_main())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
from canvasapi.assignment import Assignment
|
|
5
|
+
from canvasapi.course import Course
|
|
6
|
+
from canvasapi.submission import Submission
|
|
7
|
+
|
|
8
|
+
from ..aarg_logging import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ZeroGrades:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
course: Course,
|
|
15
|
+
assignment_group_name: str | None = None,
|
|
16
|
+
poll_interval: int = 2,
|
|
17
|
+
delay: timedelta | int | float = 0,
|
|
18
|
+
):
|
|
19
|
+
self._course = course
|
|
20
|
+
self._group_name = assignment_group_name
|
|
21
|
+
self._poll_interval = poll_interval
|
|
22
|
+
self._delay = self._coerce_delay(delay)
|
|
23
|
+
|
|
24
|
+
async def __call__(self):
|
|
25
|
+
assignment_groups = self._get_assignment_groups()
|
|
26
|
+
processed_assignment_ids = set()
|
|
27
|
+
|
|
28
|
+
assignment: Assignment
|
|
29
|
+
for assignment in self._course.get_assignments():
|
|
30
|
+
processed_assignment_ids.add(assignment.id)
|
|
31
|
+
|
|
32
|
+
if self._group_name is not None and assignment_groups[assignment.assignment_group_id] != self._group_name:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
logger.debug(
|
|
36
|
+
"Processing assignment: %s (%s)",
|
|
37
|
+
getattr(assignment, "name", assignment.id),
|
|
38
|
+
assignment.id,
|
|
39
|
+
)
|
|
40
|
+
self._zero_ungraded_past_due_submissions(assignment)
|
|
41
|
+
|
|
42
|
+
for quiz in self._course.get_quizzes():
|
|
43
|
+
assignment_id = getattr(quiz, "assignment_id", None)
|
|
44
|
+
if assignment_id is None or assignment_id in processed_assignment_ids:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
logger.debug(
|
|
48
|
+
"Processing quiz assignment: %s (%s)",
|
|
49
|
+
getattr(quiz, "title", quiz.id),
|
|
50
|
+
assignment_id,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assignment = self._course.get_assignment(assignment_id)
|
|
54
|
+
self._zero_ungraded_past_due_submissions(assignment)
|
|
55
|
+
|
|
56
|
+
def _zero_ungraded_past_due_submissions(self, assignment: Assignment):
|
|
57
|
+
due_at = getattr(assignment, "due_at", None)
|
|
58
|
+
|
|
59
|
+
if not self._is_past_due(due_at):
|
|
60
|
+
logger.debug("Assignment is not past due; leaving ungraded submissions alone")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
grade_data = {}
|
|
64
|
+
|
|
65
|
+
for submission in assignment.get_submissions():
|
|
66
|
+
if self._has_grade(submission):
|
|
67
|
+
continue
|
|
68
|
+
if self._has_submission(submission):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
grade_data[submission.user_id] = {"posted_grade": 0}
|
|
72
|
+
|
|
73
|
+
if not grade_data:
|
|
74
|
+
logger.debug("No past-due, ungraded, missing submissions found")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
"Submitting zero-grade update for %s students on assignment %s %s",
|
|
79
|
+
len(grade_data),
|
|
80
|
+
assignment.id,
|
|
81
|
+
assignment.name
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
progress = assignment.submissions_bulk_update(grade_data=grade_data)
|
|
85
|
+
self._wait_for_progress(progress)
|
|
86
|
+
|
|
87
|
+
def _is_past_due(self, due_at: str | None) -> bool:
|
|
88
|
+
due_dt = self._parse_canvas_datetime(due_at)
|
|
89
|
+
return bool(due_dt and datetime.now(timezone.utc) > due_dt + self._delay)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _coerce_delay(delay: timedelta | int | float) -> timedelta:
|
|
93
|
+
if isinstance(delay, timedelta):
|
|
94
|
+
return delay
|
|
95
|
+
return timedelta(seconds=delay)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _parse_canvas_datetime(value: str | None) -> datetime | None:
|
|
99
|
+
if not value:
|
|
100
|
+
return None
|
|
101
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _has_submission(submission: Submission) -> bool:
|
|
105
|
+
return bool(getattr(submission, "submitted_at", None))
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _has_grade(submission: Submission) -> bool:
|
|
109
|
+
return getattr(submission, "grade", None) not in (None, "")
|
|
110
|
+
|
|
111
|
+
def _wait_for_progress(self, progress):
|
|
112
|
+
while True:
|
|
113
|
+
progress = progress.query()
|
|
114
|
+
workflow_state = getattr(progress, "workflow_state", None)
|
|
115
|
+
completion = getattr(progress, "completion", None)
|
|
116
|
+
|
|
117
|
+
logger.debug(
|
|
118
|
+
"Progress %s: state=%s, completion=%s",
|
|
119
|
+
progress.id,
|
|
120
|
+
workflow_state,
|
|
121
|
+
completion,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if workflow_state == "completed":
|
|
125
|
+
return progress
|
|
126
|
+
|
|
127
|
+
if workflow_state in {"failed", "error"}:
|
|
128
|
+
message = getattr(progress, "message", "Canvas bulk grade update failed")
|
|
129
|
+
raise RuntimeError(message)
|
|
130
|
+
|
|
131
|
+
time.sleep(self._poll_interval)
|
|
132
|
+
|
|
133
|
+
def _get_assignment_groups(self):
|
|
134
|
+
return {
|
|
135
|
+
group.id: group.name
|
|
136
|
+
for group in self._course.get_assignment_groups()
|
|
137
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from asyncio import iscoroutine
|
|
2
|
+
from typing import Callable, Iterable, Coroutine
|
|
3
|
+
|
|
4
|
+
from canvasapi.course import Course
|
|
5
|
+
from canvasapi.submission import Submission
|
|
6
|
+
|
|
7
|
+
SUBMISSION_GRADER = Callable[[Submission], Coroutine] | Callable[[Submission], None]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Grader:
|
|
11
|
+
def __init__(self, course: Course, graders: list[SUBMISSION_GRADER]):
|
|
12
|
+
self._course = course
|
|
13
|
+
self._graders: list[SUBMISSION_GRADER] = graders
|
|
14
|
+
|
|
15
|
+
async def __call__(self):
|
|
16
|
+
for submission in self._retrieve_ungraded_submissions():
|
|
17
|
+
await self._grade_submission(submission)
|
|
18
|
+
|
|
19
|
+
async def _grade_submission(self, submission: Submission):
|
|
20
|
+
for grader in self._graders:
|
|
21
|
+
if iscoroutine(result := grader(submission)):
|
|
22
|
+
await result
|
|
23
|
+
|
|
24
|
+
def _retrieve_ungraded_submissions(self) -> Iterable[Submission]:
|
|
25
|
+
"""Query canvas for ungraded submissions"""
|
|
26
|
+
yield from self._course.get_multiple_submissions(
|
|
27
|
+
student_ids=["all"],
|
|
28
|
+
workflow_state=["submitted", "pending_review"],
|
|
29
|
+
include=["submission_history", "user", "assignment"]
|
|
30
|
+
)
|
|
31
|
+
"""Notes:
|
|
32
|
+
course.get_multiple_submissions(
|
|
33
|
+
student_ids=["all"],
|
|
34
|
+
# submitted_since="2026-04-01T00:00:00Z",
|
|
35
|
+
workflow_state="submitted",
|
|
36
|
+
include=["submission_history", "user", "assignment"]
|
|
37
|
+
)
|
|
38
|
+
-> returns paginated list of submissions
|
|
39
|
+
|
|
40
|
+
-- Assignments --
|
|
41
|
+
submission.submission_type=None for "not_graded" assignments (e.g. lecture days)
|
|
42
|
+
|
|
43
|
+
-- Quizzes --
|
|
44
|
+
submission.submission_type="online_quiz"
|
|
45
|
+
grader_id= negative quiz ID
|
|
46
|
+
submission_history has submission_data
|
|
47
|
+
"question_id" links to question
|
|
48
|
+
"text" has student response
|
|
49
|
+
"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Submission(
|
|
4
|
+
_requester=<canvasapi.requester.Requester object at 0x1072c2660>,
|
|
5
|
+
id=77165367, user_id=147873, url=None, score=None, grade=None,
|
|
6
|
+
excused=None, attempt=None, submission_type=None, submitted_at=None,
|
|
7
|
+
body=None, assignment_id=1303191, assignment_id_date=1303-07-10 00:00:00+00:00,
|
|
8
|
+
graded_at=None, grade_matches_current_submission=True, grader_id=None,
|
|
9
|
+
workflow_state=unsubmitted,
|
|
10
|
+
late_policy_status=None, points_deducted=None, grading_period_id=None,
|
|
11
|
+
cached_due_date=2026-01-21T06:59:00Z, cached_due_date_date=2026-01-21 06:59:00+00:00,
|
|
12
|
+
extra_attempts=None, posted_at=None, redo_request=False, sticker=None,
|
|
13
|
+
custom_grade_status_id=None, late=False, missing=False, seconds_late=9723591,
|
|
14
|
+
entered_grade=None, entered_score=None,
|
|
15
|
+
preview_url=https://byu.instructure.com/courses/28537/assignments/1303191/submissions/147873?preview=1&version=0,
|
|
16
|
+
submission_history=[{
|
|
17
|
+
'id': 77165367, 'user_id': 147873, 'url': None, 'score': None,
|
|
18
|
+
'grade': None, 'excused': None, 'attempt': None, 'submission_type': None,
|
|
19
|
+
'submitted_at': None, 'body': None, 'assignment_id': 1303191,
|
|
20
|
+
'graded_at': None, 'grade_matches_current_submission': True,
|
|
21
|
+
'grader_id': None, 'workflow_state': 'unsubmitted', 'late_policy_status': None,
|
|
22
|
+
'points_deducted': None, 'grading_period_id': None,
|
|
23
|
+
'cached_due_date': '2026-01-21T06:59:00Z', 'extra_attempts': None,
|
|
24
|
+
'posted_at': None, 'redo_request': False, 'sticker': None,
|
|
25
|
+
'custom_grade_status_id': None, 'late': False, 'missing': False,
|
|
26
|
+
'seconds_late': 9723591, 'entered_grade': None, 'entered_score': None,
|
|
27
|
+
'preview_url': 'https://byu.instructure.com/courses/28537/assignments/1303191/submissions/147873?preview=1&version=0'
|
|
28
|
+
}],
|
|
29
|
+
assignment={
|
|
30
|
+
'id': 1303191, 'position': 4, '
|
|
31
|
+
description': '<link rel="stylesheet" href="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409020/dp_app.css"><p><em>Updated on April 15, 2026 at 12:41 PM</em></p>\n<h3>Lecture files</h3>\n<p><a class="instructure_file_link inline_disabled" href="https://byu.instructure.com/courses/28537/files/12892789?verifier=NJreanaFcqNNCTbTcY2VGBeeUvWawxefUQyN4tKP&wrap=1" style="color:oklch(62.3% 0.214 259.815)" target="_blank" data-api-endpoint="https://byu.instructure.com/api/v1/courses/28537/files/12892789" data-api-returntype="File">unit1-prompt-engineering-lecture1c-chat-class_material.zip</a></p>\n<h3>Outline</h3>\n<p>Topics:</p>\n<ol>\n<li>Context<ol>\n<li>\'System\' vs \'user\' inputs</li>\n</ol>\n</li>\n<li>General chat code<ol>\n<li>Guessing game</li>\n</ol>\n</li>\n<li>Personas<ol>\n<li>Tone, style<ol>\n<li>Talk like a pirate, be brief, provide examples, use emojis, be professional</li>\n</ol>\n</li>\n<li>Roles<ol>\n<li>Tutor</li>\n<li>Student (I tutor you)</li>\n<li>Collaborator</li>\n</ol>\n</li>\n<li>Limitations of impersonation<ol>\n<li>Lack of definition<ol>\n<li>Act like a student with freshman-level experience vs senior-level experience<ol>\n<li>What does this mean? How well can it deliver? What details do you really need?</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>Lack of relevance<ol>\n<li>Act like a middle-aged white male that likes D\\&D and long-walks on the beach<ol>\n<li>Now talk tech support/womens’ shoe fashion/etc.</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>Identifying that goals have been met - Cat-dog-bird game<ol>\n<li>AIs struggle to stop talking. How do you prompt a clear, accurate exit?</li>\n<li>How does an AI know when a task is finished?<ol>\n<li>"Before solving the customer\'s problem, first make sure you understand their situation clearly."</li>\n</ol>\n</li>\n</ol>\n</li>\n<li>Hallucination<ol>\n<li>Training data limitations (ask about recent events, obscure topics, etc.)</li>\n</ol>\n</li>\n</ol><script src="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409019/dp_app.js"></script>',
|
|
32
|
+
'points_possible': None, 'grading_type': 'points',
|
|
33
|
+
'created_at': '2026-01-08T18:43:46Z', 'updated_at': '2026-04-15T18:41:16Z',
|
|
34
|
+
'due_at': '2026-01-21T06:59:00Z', 'final_grader_id': None,
|
|
35
|
+
'grader_count': 0, 'graders_anonymous_to_graders': False,
|
|
36
|
+
'grader_comments_visible_to_graders': True,
|
|
37
|
+
'grader_names_visible_to_final_grader': True, 'lock_at': None,
|
|
38
|
+
'unlock_at': None, 'assignment_group_id': 153985,
|
|
39
|
+
'peer_reviews': False, 'anonymous_peer_reviews': False,
|
|
40
|
+
'automatic_peer_reviews': False, 'intra_group_peer_reviews': False,
|
|
41
|
+
'post_to_sis': False, 'grade_group_students_individually': False,
|
|
42
|
+
'group_category_id': None, 'grading_standard_id': None,
|
|
43
|
+
'moderated_grading': False, 'hide_in_gradebook': False,
|
|
44
|
+
'omit_from_final_grade': False, 'suppress_assignment': False,
|
|
45
|
+
'anonymous_instructor_annotations': False, 'anonymous_grading': False,
|
|
46
|
+
'allowed_attempts': -1, 'annotatable_attachment_id': None,
|
|
47
|
+
'secure_params': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsdGlfYXNzaWdubWVudF9pZCI6ImM2ODc0ZmIyLTY1NWQtNDk0My1iYTI5LWE4OGNlODE0MWI5ZCIsImx0aV9hc3NpZ25tZW50X2Rlc2NyaXB0aW9uIjoiXHUwMDNjcFx1MDAzZVx1MDAzY2VtXHUwMDNlVXBkYXRlZCBvbiBBcHJpbCAxNSwgMjAyNiBhdCAxMjo0MSBQTVx1MDAzYy9lbVx1MDAzZVx1MDAzYy9wXHUwMDNlXG5cdTAwM2NoM1x1MDAzZUxlY3R1cmUgZmlsZXNcdTAwM2MvaDNcdTAwM2Vcblx1MDAzY3BcdTAwM2VcdTAwM2NhIGNsYXNzPVwiaW5zdHJ1Y3R1cmVfZmlsZV9saW5rIGlubGluZV9kaXNhYmxlZFwiIGhyZWY9XCIvY291cnNlcy8yODUzNy9maWxlcy8xMjg5Mjc4OT93cmFwPTFcIiBzdHlsZT1cImNvbG9yOm9rbGNoKDYyLjMlIDAuMjE0IDI1OS44MTUpXCIgdGFyZ2V0PVwiX2JsYW5rXCJcdTAwM2V1bml0MS1wcm9tcHQtZW5naW5lZXJpbmctbGVjdHVyZTFjLWNoYXQtY2xhc3NfbWF0ZXJpYWwuemlwXHUwMDNjL2FcdTAwM2VcdTAwM2MvcFx1MDAzZVxuXHUwMDNjaDNcdTAwM2VPdXRsaW5lXHUwMDNjL2gzXHUwMDNlXG5cdTAwM2NwXHUwMDNlVG9waWNzOlx1MDAzYy9wXHUwMDNlXG5cdTAwM2NvbFx1MDAzZVxuXHUwMDNjbGlcdTAwM2VDb250ZXh0XHUwMDNjb2xcdTAwM2Vcblx1MDAzY2xpXHUwMDNlJ1N5c3RlbScgdnMgJ3VzZXInIGlucHV0c1x1MDAzYy9saVx1MDAzZVxuXHUwMDNjL29sXHUwMDNlXG5cdTAwM2MvbGlcdTAwM2Vcblx1MDAzY2xpXHUwMDNlR2VuZXJhbCBjaGF0IGNvZGVcdTAwM2NvbFx1MDAzZVxuXHUwMDNjbGlcdTAwM2VHdWVzc2luZyBnYW1lXHUwMDNjL2xpXHUwMDNlXG5cdTAwM2Mvb2xcdTAwM2Vcblx1MDAzYy9saVx1MDAzZVxuXHUwMDNjbGlcdTAwM2VQZXJzb25hc1x1MDAzY29sXHUwMDNlXG5cdTAwM2NsaVx1MDAzZVRvbmUsIHN0eWxlXHUwMDNjb2xcdTAwM2Vcblx1MDAzY2xpXHUwMDNlVGFsayBsaWtlIGEgcGlyYXRlLCBiZSBicmllZiwgcHJvdmlkZSBleGFtcGxlcywgdXNlIGVtb2ppcywgYmUgcHJvZmVzc2lvbmFsXHUwMDNjL2xpXHUwMDNlXG5cdTAwM2Mvb2xcdTAwM2Vcblx1MDAzYy9saVx1MDAzZVxuXHUwMDNjbGlcdTAwM2VSb2xlc1x1MDAzY29sXHUwMDNlXG5cdTAwM2NsaVx1MDAzZVR1dG9yXHUwMDNjL2xpXHUwMDNlXG5cdTAwM2NsaVx1MDAzZVN0dWRlbnQgKEkgdHV0b3IgeW91KVx1MDAzYy9saVx1MDAzZVxuXHUwMDNjbGlcdTAwM2VDb2xsYWJvcmF0b3JcdTAwM2MvbGlcdTAwM2Vcblx1MDAzYy9vbFx1MDAzZVxuXHUwMDNjL2xpXHUwMDNlXG5cdTAwM2NsaVx1MDAzZUxpbWl0YXRpb25zIG9mIGltcGVyc29uYXRpb25cdTAwM2NvbFx1MDAzZVxuXHUwMDNjbGlcdTAwM2VMYWNrIG9mIGRlZmluaXRpb25cdTAwM2NvbFx1MDAzZVxuXHUwMDNjbGlcdTAwM2VBY3QgbGlrZSBhIHN0dWRlbnQgd2l0aCBmcmVzaG1hbi1sZXZlbCBleHBlcmllbmNlIHZzIHNlbmlvci1sZXZlbCBleHBlcmllbmNlXHUwMDNjb2xcdTAwM2Vcblx1MDAzY2xpXHUwMDNlV2hhdCBkb2VzIHRoaXMgbWVhbj8gSG93IHdlbGwgY2FuIGl0IGRlbGl2ZXI_IFdoYXQgZGV0YWlscyBkbyB5b3UgcmVhbGx5IG5lZWQ_XHUwMDNjL2xpXHUwMDNlXG5cdTAwM2Mvb2xcdTAwM2Vcblx1MDAzYy9saVx1MDAzZVxuXHUwMDNjL29sXHUwMDNlXG5cdTAwM2MvbGlcdTAwM2Vcblx1MDAzY2xpXHUwMDNlTGFjayBvZiByZWxldmFuY2VcdTAwM2NvbFx1MDAzZVxuXHUwMDNjbGlcdTAwM2VBY3QgbGlrZS4uLiAodHJ1bmNhdGVkKSJ9.d06cGzFaerfUIlgxPIt3w69EZOgciY6MrmxX1n7K_hE',
|
|
48
|
+
'lti_context_id': 'c6874fb2-655d-4943-ba29-a88ce8141b9d', 'course_id': 28537,
|
|
49
|
+
'name': 'Lecture 1c - Chat', 'submission_types': ['not_graded'],
|
|
50
|
+
'has_submitted_submissions': False, 'due_date_required': False,
|
|
51
|
+
'max_name_length': 255, 'in_closed_grading_period': False,
|
|
52
|
+
'graded_submissions_exist': False, 'is_quiz_assignment': False,
|
|
53
|
+
'can_duplicate': True, 'original_course_id': None,
|
|
54
|
+
'original_assignment_id': None, 'original_lti_resource_link_id': None,
|
|
55
|
+
'original_assignment_name': None, 'original_quiz_id': None,
|
|
56
|
+
'workflow_state': 'published', 'important_dates': False, 'muted': True,
|
|
57
|
+
'html_url': 'https://byu.instructure.com/courses/28537/assignments/1303191',
|
|
58
|
+
'speed_grader_url': 'https://byu.instructure.com/courses/28537/gradebook/speed_grader?assignment_id=1303191',
|
|
59
|
+
'has_overrides': False, 'needs_grading_count': 0, 'sis_assignment_id': None,
|
|
60
|
+
'integration_id': None, 'integration_data': {}, 'published': True,
|
|
61
|
+
'unpublishable': True, 'only_visible_to_overrides': False,
|
|
62
|
+
'visible_to_everyone': True, 'locked_for_user': False,
|
|
63
|
+
'submissions_download_url': 'https://byu.instructure.com/courses/28537/assignments/1303191/submissions?zip=1',
|
|
64
|
+
'post_manually': False, 'anonymize_students': False,
|
|
65
|
+
'new_quizzes_anonymous_participants': False, 'require_lockdown_browser': False,
|
|
66
|
+
'restrict_quantitative_data': False
|
|
67
|
+
},
|
|
68
|
+
user={
|
|
69
|
+
'id': 147873, 'name': 'Test Student', 'created_at': '2025-05-28T15:52:39Z',
|
|
70
|
+
'sortable_name': 'Student, Test', 'short_name': 'Test Student',
|
|
71
|
+
'sis_user_id': None, 'integration_id': None,
|
|
72
|
+
'login_id': 'fcf0d54b9eb9851342c1f80c095c3866fafb5837',
|
|
73
|
+
'avatar_url': 'https://byu.instructure.com/images/messages/avatar-50.png'
|
|
74
|
+
},
|
|
75
|
+
course_id=28537, attachments=[]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
Submission(
|
|
83
|
+
_requester=<canvasapi.requester.Requester object at 0x10a936660>,
|
|
84
|
+
id=77165372, user_id=147873, url=None, score=1.03, grade=1.03, excused=None,
|
|
85
|
+
attempt=2, submission_type=online_quiz, submitted_at=2026-02-27T22:41:05Z,
|
|
86
|
+
submitted_at_date=2026-02-27 22:41:05+00:00,
|
|
87
|
+
body=<link rel="stylesheet" href="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409020/dp_app.css">user: 147873, quiz: 596422, score: 1.03, time: 2026-02-27 15:41:36 -0700<script src="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409019/dp_app.js"></script>,
|
|
88
|
+
assignment_id=1303196, assignment_id_date=1303-07-15 00:00:00+00:00,
|
|
89
|
+
graded_at=2026-02-27T22:41:36Z, graded_at_date=2026-02-27 22:41:36+00:00,
|
|
90
|
+
grade_matches_current_submission=True, grader_id=-596422, workflow_state=graded,
|
|
91
|
+
late_policy_status=None, points_deducted=0.0, grading_period_id=None,
|
|
92
|
+
cached_due_date=2026-01-22T06:59:00Z, cached_due_date_date=2026-01-22 06:59:00+00:00,
|
|
93
|
+
extra_attempts=None, posted_at=2026-01-23T23:23:10Z,
|
|
94
|
+
posted_at_date=2026-01-23 23:23:10+00:00, redo_request=False, sticker=None,
|
|
95
|
+
custom_grade_status_id=None, late=True, missing=False, seconds_late=3166865,
|
|
96
|
+
entered_grade=1.03, entered_score=1.03,
|
|
97
|
+
preview_url=https://byu.instructure.com/courses/28537/assignments/1303196/submissions/147873?preview=1&version=2,
|
|
98
|
+
submission_history=[{
|
|
99
|
+
'id': 15234095, 'user_id': 147873, 'url': None, 'score': 1.03, 'grade': '1.03',
|
|
100
|
+
'excused': None, 'attempt': 2, 'submission_type': 'online_quiz',
|
|
101
|
+
'submitted_at': '2026-02-27T22:41:05Z', 'body': None,
|
|
102
|
+
'assignment_id': 1303196, 'graded_at': None,
|
|
103
|
+
'grade_matches_current_submission': True,
|
|
104
|
+
'grader_id': -596422,
|
|
105
|
+
'workflow_state': 'complete', 'late_policy_status': None,
|
|
106
|
+
'points_deducted': 0.0, 'grading_period_id': None,
|
|
107
|
+
'cached_due_date': '2026-01-22T06:59:00Z', 'extra_attempts': None,
|
|
108
|
+
'posted_at': '2026-01-23T23:23:10Z', 'redo_request': False, 'sticker': None,
|
|
109
|
+
'custom_grade_status_id': None, 'late': True, 'missing': False,
|
|
110
|
+
'seconds_late': 3166865, 'entered_grade': '1.03',
|
|
111
|
+
'entered_score': 1.03,
|
|
112
|
+
'preview_url': 'https://byu.instructure.com/courses/28537/assignments/1303196/submissions/147873?preview=1&version=2',
|
|
113
|
+
'submission_data': [
|
|
114
|
+
{'correct': 'defined', 'points': 1.0, 'question_id': 6579900, 'text': '<p>System is from the computer; user is from the user</p>'},
|
|
115
|
+
{'correct': 'defined', 'points': 0.01, 'question_id': 6628286, 'text': '<p>System is from the computer; user is from the user</p>'},
|
|
116
|
+
{'correct': 'defined', 'points': 0.01, 'question_id': 6579901, 'text': ''},
|
|
117
|
+
{'correct': 'defined', 'points': 0.01, 'question_id': 6628285, 'text': ''}
|
|
118
|
+
]},
|
|
119
|
+
{'id': 15234095, 'user_id': 147873, 'url': None, 'score': 0.02,
|
|
120
|
+
'grade': '1.03', 'excused': None, 'attempt': 1,
|
|
121
|
+
'submission_type': 'online_quiz', 'submitted_at': '2026-01-23T23:23:10Z',
|
|
122
|
+
'body': None, 'assignment_id': 1303196, 'graded_at': None, 'grade_matches_current_submission': True, 'grader_id': -596422, 'workflow_state': 'complete', 'late_policy_status': None, 'points_deducted': 0.0, 'grading_period_id': None, 'cached_due_date': '2026-01-22T06:59:00Z', 'extra_attempts': None, 'posted_at': '2026-01-23T23:23:10Z', 'redo_request': False, 'sticker': None, 'custom_grade_status_id': None, 'late': True, 'missing': False, 'seconds_late': 3166865, 'entered_grade': '1.03', 'entered_score': 1.03, 'preview_url': 'https://byu.instructure.com/courses/28537/assignments/1303196/submissions/147873?preview=1&version=1', 'submission_data': [{'correct': 'defined', 'points': 0.01, 'question_id': 6546157, 'text': '<p>La la la </p>'}, {'correct': 'defined', 'points': 0.01, 'question_id': 6546158, 'text': '<p>la la la </p>'}]}], assignment={'id': 1303196, 'position': 5, 'description': '<link rel="stylesheet" href="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409020/dp_app.css"><p>Possible interview questions from 1c - Chat.</p><script src="https://instructure-uploads.s3.amazonaws.com/account_74070000000000001/attachments/8409019/dp_app.js"></script>', 'points_possible': 4.0, 'grading_type': 'points', 'created_at': '2026-01-08T18:43:54Z', 'updated_at': '2026-04-13T19:33:21Z', 'due_at': '2026-01-22T06:59:00Z', 'final_grader_id': None, 'grader_count': 0, 'graders_anonymous_to_graders': False, 'grader_comments_visible_to_graders': True, 'grader_names_visible_to_final_grader': True, 'lock_at': '2026-04-16T05:59:00Z', 'unlock_at': None, 'assignment_group_id': 153984, 'peer_reviews': False, 'anonymous_peer_reviews': False, 'automatic_peer_reviews': False, 'intra_group_peer_reviews': False, 'post_to_sis': False, 'grade_group_students_individually': False, 'group_category_id': None, 'grading_standard_id': None, 'moderated_grading': False, 'hide_in_gradebook': False, 'omit_from_final_grade': False, 'suppress_assignment': False, 'anonymous_instructor_annotations': False, 'anonymous_grading': False, 'allowed_attempts': -1, 'annotatable_attachment_id': None, 'secure_params': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsdGlfYXNzaWdubWVudF9pZCI6Ijc5Y2U2ZTIwLTNiMjUtNGQ5ZC04ZTMxLWMxNmVlYmNhOWFiZSIsImx0aV9hc3NpZ25tZW50X2Rlc2NyaXB0aW9uIjoiXHUwMDNjcFx1MDAzZVBvc3NpYmxlIGludGVydmlldyBxdWVzdGlvbnMgZnJvbSAxYyAtIENoYXQuXHUwMDNjL3BcdTAwM2UifQ.-CXfIiABO4QN540uaF3EnQDSoZ1ginhZtwNjijCDAG4', 'lti_context_id': '79ce6e20-3b25-4d9d-8e31-c16eebca9abe', 'course_id': 28537, 'name': '1c Interview Questions', 'submission_types': ['online_quiz'], 'has_submitted_submissions': True, 'due_date_required': False, 'max_name_length': 255, 'in_closed_grading_period': False, 'availability_status': {'status': 'closed', 'date': None}, 'graded_submissions_exist': True, 'is_quiz_assignment': True, 'can_duplicate': False, 'original_course_id': None, 'original_assignment_id': None, 'original_lti_resource_link_id': None, 'original_assignment_name': None, 'original_quiz_id': None, 'workflow_state': 'published', 'important_dates': False, 'muted': False, 'html_url': 'https://byu.instructure.com/courses/28537/assignments/1303196', 'speed_grader_url': 'https://byu.instructure.com/courses/28537/gradebook/speed_grader?assignment_id=1303196', 'has_overrides': False, 'needs_grading_count': 0, 'sis_assignment_id': None, 'integration_id': None, 'integration_data': {}, 'quiz_id': 596422, 'anonymous_submissions': False, 'published': True, 'unpublishable': False, 'only_visible_to_overrides': False, 'visible_to_everyone': True, 'locked_for_user': False, 'submissions_download_url': 'https://byu.instructure.com/courses/28537/quizzes/596422/submissions?zip=1', 'post_manually': False, 'anonymize_students': False, 'new_quizzes_anonymous_participants': False, 'require_lockdown_browser': False, 'restrict_quantitative_data': False}, user={'id': 147873, 'name': 'Test Student', 'created_at': '2025-05-28T15:52:39Z', 'sortable_name': 'Student, Test', 'short_name': 'Test Student', 'sis_user_id': None, 'integration_id': None, 'login_id': 'fcf0d54b9eb9851342c1f80c095c3866fafb5837', 'avatar_url': 'https://byu.instructure.com/images/messages/avatar-50.png'}, course_id=28537, attachments=[])
|
|
123
|
+
"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Server:
|
|
5
|
+
"""Schedule jobs in asyncio tasks."""
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._tasks = []
|
|
9
|
+
|
|
10
|
+
def schedule(self, interval, job):
|
|
11
|
+
self._tasks.append(self._run(interval, job))
|
|
12
|
+
|
|
13
|
+
async def _run(self, interval, job):
|
|
14
|
+
while True:
|
|
15
|
+
await job() # todo - handle sync and async job signatures
|
|
16
|
+
await asyncio.sleep(interval)
|
|
17
|
+
|
|
18
|
+
async def start(self):
|
|
19
|
+
await asyncio.gather(*self._tasks)
|