assignment-codeval 0.0.23__tar.gz → 0.0.24__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.
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/PKG-INFO +1 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/pyproject.toml +1 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/canvas_utils.py +75 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/check_grading.py +7 -79
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/recent_comments.py +55 -36
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/PKG-INFO +1 -1
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/README.md +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/setup.cfg +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/__init__.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/ai_benchmark.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/cli.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/commons.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/convertMD2Html.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/create_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/evaluate.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/file_utils.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/github_connect.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/install_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/submissions.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/requires.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/top_level.txt +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_check_grading.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_codeval.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_create_assignment.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_evaluate_submissions.py +0 -0
- {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_install_assignment.py +0 -0
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/canvas_utils.py
RENAMED
|
@@ -5,6 +5,7 @@ from functools import cache
|
|
|
5
5
|
from typing import NamedTuple
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
+
import requests
|
|
8
9
|
from canvasapi import Canvas
|
|
9
10
|
from canvasapi.current_user import CurrentUser
|
|
10
11
|
|
|
@@ -107,3 +108,77 @@ def get_assignment(course, assignment_name):
|
|
|
107
108
|
sys.exit(2)
|
|
108
109
|
assignment = assignments[0]
|
|
109
110
|
return assignment
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
SUBMISSIONS_QUERY = """
|
|
114
|
+
query SubmissionsQuery($assignmentId: ID!, $cursor: String) {
|
|
115
|
+
assignment(id: $assignmentId) {
|
|
116
|
+
submissionsConnection(after: $cursor) {
|
|
117
|
+
pageInfo {
|
|
118
|
+
hasNextPage
|
|
119
|
+
endCursor
|
|
120
|
+
}
|
|
121
|
+
nodes {
|
|
122
|
+
_id
|
|
123
|
+
submittedAt
|
|
124
|
+
user {
|
|
125
|
+
name
|
|
126
|
+
}
|
|
127
|
+
commentsConnection(filter: {allComments: true}, sortOrder: desc) {
|
|
128
|
+
nodes {
|
|
129
|
+
comment
|
|
130
|
+
createdAt
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_canvas_credentials():
|
|
141
|
+
"""Read raw url and token from codeval.ini config file."""
|
|
142
|
+
parser = ConfigParser()
|
|
143
|
+
config_file = click.get_app_dir("codeval.ini")
|
|
144
|
+
parser.read(config_file)
|
|
145
|
+
if 'SERVER' not in parser:
|
|
146
|
+
error(f"did not find [SERVER] section in {config_file}.")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
for key in ['url', 'token']:
|
|
149
|
+
if key not in parser['SERVER']:
|
|
150
|
+
error(f"did not find {key} in [SERVER] in {config_file}.")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
return parser['SERVER']['url'], parser['SERVER']['token']
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def graphql_request(base_url, token, query, variables):
|
|
156
|
+
"""POST a GraphQL query to Canvas and return the data."""
|
|
157
|
+
url = f"{base_url}/api/graphql"
|
|
158
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
159
|
+
payload = {"query": query, "variables": variables}
|
|
160
|
+
resp = requests.post(url, json=payload, headers=headers)
|
|
161
|
+
resp.raise_for_status()
|
|
162
|
+
result = resp.json()
|
|
163
|
+
if "errors" in result:
|
|
164
|
+
error(f"GraphQL errors: {result['errors']}")
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
return result["data"]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fetch_all_submissions(base_url, token, assignment_id):
|
|
170
|
+
"""Fetch all submissions for an assignment using relay pagination."""
|
|
171
|
+
all_nodes = []
|
|
172
|
+
cursor = None
|
|
173
|
+
while True:
|
|
174
|
+
variables = {"assignmentId": str(assignment_id)}
|
|
175
|
+
if cursor:
|
|
176
|
+
variables["cursor"] = cursor
|
|
177
|
+
data = graphql_request(base_url, token, SUBMISSIONS_QUERY, variables)
|
|
178
|
+
conn = data["assignment"]["submissionsConnection"]
|
|
179
|
+
all_nodes.extend(conn["nodes"])
|
|
180
|
+
if conn["pageInfo"]["hasNextPage"]:
|
|
181
|
+
cursor = conn["pageInfo"]["endCursor"]
|
|
182
|
+
else:
|
|
183
|
+
break
|
|
184
|
+
return all_nodes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/check_grading.py
RENAMED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""Check which submissions are missing a codeval comment newer than the submission."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from configparser import ConfigParser
|
|
5
4
|
from datetime import datetime, timedelta, timezone
|
|
6
5
|
from math import floor
|
|
7
6
|
|
|
8
7
|
import click
|
|
9
|
-
import requests
|
|
10
8
|
|
|
11
|
-
from assignment_codeval.canvas_utils import
|
|
9
|
+
from assignment_codeval.canvas_utils import (
|
|
10
|
+
connect_to_canvas, get_course,
|
|
11
|
+
get_canvas_credentials, graphql_request, fetch_all_submissions,
|
|
12
|
+
)
|
|
12
13
|
from assignment_codeval.commons import debug, error
|
|
13
14
|
|
|
14
15
|
ASSIGNMENTS_QUERY = """
|
|
@@ -29,79 +30,6 @@ query AssignmentsQuery($courseId: ID!) {
|
|
|
29
30
|
}
|
|
30
31
|
"""
|
|
31
32
|
|
|
32
|
-
SUBMISSIONS_QUERY = """
|
|
33
|
-
query SubmissionsQuery($assignmentId: ID!, $cursor: String) {
|
|
34
|
-
assignment(id: $assignmentId) {
|
|
35
|
-
submissionsConnection(after: $cursor) {
|
|
36
|
-
pageInfo {
|
|
37
|
-
hasNextPage
|
|
38
|
-
endCursor
|
|
39
|
-
}
|
|
40
|
-
nodes {
|
|
41
|
-
_id
|
|
42
|
-
submittedAt
|
|
43
|
-
user {
|
|
44
|
-
name
|
|
45
|
-
}
|
|
46
|
-
commentsConnection(filter: {allComments: true}) {
|
|
47
|
-
nodes {
|
|
48
|
-
comment
|
|
49
|
-
createdAt
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _get_canvas_credentials():
|
|
60
|
-
"""Read raw url and token from codeval.ini config file."""
|
|
61
|
-
parser = ConfigParser()
|
|
62
|
-
config_file = click.get_app_dir("codeval.ini")
|
|
63
|
-
parser.read(config_file)
|
|
64
|
-
if 'SERVER' not in parser:
|
|
65
|
-
error(f"did not find [SERVER] section in {config_file}.")
|
|
66
|
-
sys.exit(1)
|
|
67
|
-
for key in ['url', 'token']:
|
|
68
|
-
if key not in parser['SERVER']:
|
|
69
|
-
error(f"did not find {key} in [SERVER] in {config_file}.")
|
|
70
|
-
sys.exit(1)
|
|
71
|
-
return parser['SERVER']['url'], parser['SERVER']['token']
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _graphql_request(base_url, token, query, variables):
|
|
75
|
-
"""POST a GraphQL query to Canvas and return the data."""
|
|
76
|
-
url = f"{base_url}/api/graphql"
|
|
77
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
78
|
-
payload = {"query": query, "variables": variables}
|
|
79
|
-
resp = requests.post(url, json=payload, headers=headers)
|
|
80
|
-
resp.raise_for_status()
|
|
81
|
-
result = resp.json()
|
|
82
|
-
if "errors" in result:
|
|
83
|
-
error(f"GraphQL errors: {result['errors']}")
|
|
84
|
-
sys.exit(1)
|
|
85
|
-
return result["data"]
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _fetch_all_submissions(base_url, token, assignment_id):
|
|
89
|
-
"""Fetch all submissions for an assignment using relay pagination."""
|
|
90
|
-
all_nodes = []
|
|
91
|
-
cursor = None
|
|
92
|
-
while True:
|
|
93
|
-
variables = {"assignmentId": str(assignment_id)}
|
|
94
|
-
if cursor:
|
|
95
|
-
variables["cursor"] = cursor
|
|
96
|
-
data = _graphql_request(base_url, token, SUBMISSIONS_QUERY, variables)
|
|
97
|
-
conn = data["assignment"]["submissionsConnection"]
|
|
98
|
-
all_nodes.extend(conn["nodes"])
|
|
99
|
-
if conn["pageInfo"]["hasNextPage"]:
|
|
100
|
-
cursor = conn["pageInfo"]["endCursor"]
|
|
101
|
-
else:
|
|
102
|
-
break
|
|
103
|
-
return all_nodes
|
|
104
|
-
|
|
105
33
|
|
|
106
34
|
def _format_elapsed(submitted_at_str):
|
|
107
35
|
"""Format elapsed time since submission as a human-readable string."""
|
|
@@ -160,10 +88,10 @@ def check_grading(course_name, assignment_group, codeval_prefix, verbose, warn,
|
|
|
160
88
|
"""
|
|
161
89
|
(canvas, user) = connect_to_canvas()
|
|
162
90
|
course = get_course(canvas, course_name)
|
|
163
|
-
base_url, token =
|
|
91
|
+
base_url, token = get_canvas_credentials()
|
|
164
92
|
|
|
165
93
|
debug(f"fetching assignment groups for course {course.name} (id={course.id})")
|
|
166
|
-
data =
|
|
94
|
+
data = graphql_request(base_url, token, ASSIGNMENTS_QUERY, {"courseId": str(course.id)})
|
|
167
95
|
|
|
168
96
|
groups = data["course"]["assignmentGroupsConnection"]["nodes"]
|
|
169
97
|
target_group = None
|
|
@@ -192,7 +120,7 @@ def check_grading(course_name, assignment_group, codeval_prefix, verbose, warn,
|
|
|
192
120
|
assignment_name = assignment["name"]
|
|
193
121
|
debug(f"checking assignment: {assignment_name}")
|
|
194
122
|
|
|
195
|
-
submissions =
|
|
123
|
+
submissions = fetch_all_submissions(base_url, token, assignment_id)
|
|
196
124
|
|
|
197
125
|
missing = []
|
|
198
126
|
warned = []
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/recent_comments.py
RENAMED
|
@@ -7,9 +7,25 @@ from datetime import datetime, timedelta, timezone
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
|
-
from assignment_codeval.canvas_utils import
|
|
10
|
+
from assignment_codeval.canvas_utils import (
|
|
11
|
+
connect_to_canvas, get_course, get_courses,
|
|
12
|
+
get_canvas_credentials, graphql_request, fetch_all_submissions,
|
|
13
|
+
)
|
|
11
14
|
from assignment_codeval.commons import despace
|
|
12
15
|
|
|
16
|
+
COURSE_ASSIGNMENTS_QUERY = """
|
|
17
|
+
query CourseAssignmentsQuery($courseId: ID!) {
|
|
18
|
+
course(id: $courseId) {
|
|
19
|
+
assignmentsConnection {
|
|
20
|
+
nodes {
|
|
21
|
+
_id
|
|
22
|
+
name
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
13
29
|
|
|
14
30
|
def format_comment_preview(comment: str, prefix: str) -> str:
|
|
15
31
|
"""Format a comment showing first 3 and last 3 lines.
|
|
@@ -127,67 +143,70 @@ def recent_comments(course_name, active, time_period, codeval_prefix, verbose, s
|
|
|
127
143
|
else:
|
|
128
144
|
courses = [get_course(canvas, course_name)]
|
|
129
145
|
|
|
146
|
+
base_url, token = get_canvas_credentials()
|
|
130
147
|
total_comments = 0
|
|
131
148
|
total_uncommented = 0
|
|
132
149
|
|
|
133
150
|
for course in courses:
|
|
134
151
|
if verbose:
|
|
135
152
|
click.echo(f"Checking course: {course.name}", err=True)
|
|
136
|
-
|
|
153
|
+
|
|
154
|
+
data = graphql_request(base_url, token, COURSE_ASSIGNMENTS_QUERY,
|
|
155
|
+
{"courseId": str(course.id)})
|
|
156
|
+
assignments = data["course"]["assignmentsConnection"]["nodes"]
|
|
157
|
+
|
|
158
|
+
for assignment in assignments:
|
|
159
|
+
assignment_name = assignment["name"]
|
|
160
|
+
assignment_id = assignment["_id"]
|
|
137
161
|
if verbose:
|
|
138
|
-
click.echo(f" Checking assignment: {
|
|
162
|
+
click.echo(f" Checking assignment: {assignment_name}", err=True)
|
|
139
163
|
|
|
140
164
|
# Check if this assignment has a codeval file
|
|
141
165
|
has_codeval = True
|
|
142
166
|
if codeval_dir:
|
|
143
|
-
codeval_file = os.path.join(codeval_dir, f"{despace(
|
|
167
|
+
codeval_file = os.path.join(codeval_dir, f"{despace(assignment_name)}.codeval")
|
|
144
168
|
has_codeval = os.path.exists(codeval_file)
|
|
145
169
|
if not has_codeval:
|
|
146
170
|
if verbose:
|
|
147
171
|
click.echo(f" (no codeval file)", err=True)
|
|
148
172
|
continue
|
|
149
173
|
|
|
174
|
+
submissions = fetch_all_submissions(base_url, token, assignment_id)
|
|
150
175
|
assignment_comments = []
|
|
151
176
|
uncommented_submissions = []
|
|
152
177
|
|
|
153
|
-
for
|
|
154
|
-
student_name =
|
|
178
|
+
for sub in submissions:
|
|
179
|
+
student_name = sub.get("user", {}).get("name", "Unknown")
|
|
180
|
+
comments = sub.get("commentsConnection", {}).get("nodes", [])
|
|
155
181
|
|
|
156
182
|
# Find codeval comments
|
|
157
183
|
codeval_comments = []
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
for comment in comments:
|
|
185
|
+
comment_text = comment.get("comment", "")
|
|
186
|
+
if not comment_text.startswith(codeval_prefix):
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
created_at_str = comment.get("createdAt", "")
|
|
190
|
+
if not created_at_str:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
created_at = datetime.fromisoformat(created_at_str)
|
|
194
|
+
codeval_comments.append({
|
|
195
|
+
'time': created_at,
|
|
196
|
+
'comment': comment_text
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if created_at >= cutoff_time:
|
|
200
|
+
assignment_comments.append({
|
|
201
|
+
'student': student_name,
|
|
174
202
|
'time': created_at,
|
|
175
203
|
'comment': comment_text
|
|
176
204
|
})
|
|
177
205
|
|
|
178
|
-
if created_at >= cutoff_time:
|
|
179
|
-
assignment_comments.append({
|
|
180
|
-
'student': student_name,
|
|
181
|
-
'time': created_at,
|
|
182
|
-
'comment': comment_text
|
|
183
|
-
})
|
|
184
|
-
|
|
185
206
|
# Check for uncommented submissions
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
except ValueError:
|
|
190
|
-
continue
|
|
207
|
+
submitted_at_str = sub.get("submittedAt")
|
|
208
|
+
if show_uncommented and has_codeval and submitted_at_str:
|
|
209
|
+
submitted_at = datetime.fromisoformat(submitted_at_str)
|
|
191
210
|
|
|
192
211
|
# Find the most recent codeval comment
|
|
193
212
|
last_codeval_time = None
|
|
@@ -203,7 +222,7 @@ def recent_comments(course_name, active, time_period, codeval_prefix, verbose, s
|
|
|
203
222
|
})
|
|
204
223
|
|
|
205
224
|
if assignment_comments:
|
|
206
|
-
click.echo(f"\n{course.name}: {
|
|
225
|
+
click.echo(f"\n{course.name}: {assignment_name}")
|
|
207
226
|
click.echo("-" * 60)
|
|
208
227
|
for c in sorted(assignment_comments, key=lambda x: x['time']):
|
|
209
228
|
time_str = format_local_time(c['time'])
|
|
@@ -214,7 +233,7 @@ def recent_comments(course_name, active, time_period, codeval_prefix, verbose, s
|
|
|
214
233
|
|
|
215
234
|
if uncommented_submissions:
|
|
216
235
|
if not assignment_comments:
|
|
217
|
-
click.echo(f"\n{course.name}: {
|
|
236
|
+
click.echo(f"\n{course.name}: {assignment_name}")
|
|
218
237
|
click.echo("-" * 60)
|
|
219
238
|
click.echo(" Uncommented submissions:")
|
|
220
239
|
for s in sorted(uncommented_submissions, key=lambda x: x['submitted']):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/ai_benchmark.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/convertMD2Html.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/create_assignment.py
RENAMED
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/file_utils.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/github_connect.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/install_assignment.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/submissions.py
RENAMED
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|