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.
Files changed (29) hide show
  1. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/PKG-INFO +1 -1
  2. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/pyproject.toml +1 -1
  3. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/canvas_utils.py +75 -0
  4. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/check_grading.py +7 -79
  5. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/recent_comments.py +55 -36
  6. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/PKG-INFO +1 -1
  7. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/README.md +0 -0
  8. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/setup.cfg +0 -0
  9. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/__init__.py +0 -0
  10. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/ai_benchmark.py +0 -0
  11. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/cli.py +0 -0
  12. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/commons.py +0 -0
  13. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/convertMD2Html.py +0 -0
  14. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/create_assignment.py +0 -0
  15. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/evaluate.py +0 -0
  16. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/file_utils.py +0 -0
  17. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/github_connect.py +0 -0
  18. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/install_assignment.py +0 -0
  19. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval/submissions.py +0 -0
  20. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/SOURCES.txt +0 -0
  21. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/dependency_links.txt +0 -0
  22. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/entry_points.txt +0 -0
  23. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/requires.txt +0 -0
  24. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/src/assignment_codeval.egg-info/top_level.txt +0 -0
  25. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_check_grading.py +0 -0
  26. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_codeval.py +0 -0
  27. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_create_assignment.py +0 -0
  28. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_evaluate_submissions.py +0 -0
  29. {assignment_codeval-0.0.23 → assignment_codeval-0.0.24}/tests/test_install_assignment.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.23
3
+ Version: 0.0.24
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assignment-codeval"
7
- version = "0.0.23"
7
+ version = "0.0.24"
8
8
  description = "CodEval for evaluating programming assignments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
@@ -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 connect_to_canvas, get_course
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 = _get_canvas_credentials()
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 = _graphql_request(base_url, token, ASSIGNMENTS_QUERY, {"courseId": str(course.id)})
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 = _fetch_all_submissions(base_url, token, assignment_id)
123
+ submissions = fetch_all_submissions(base_url, token, assignment_id)
196
124
 
197
125
  missing = []
198
126
  warned = []
@@ -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 connect_to_canvas, get_course, get_courses
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
- for assignment in course.get_assignments():
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: {assignment.name}", err=True)
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(assignment.name)}.codeval")
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 submission in assignment.get_submissions(include=["submission_comments", "user"]):
154
- student_name = submission.user.get('name', 'Unknown') if submission.user else 'Unknown'
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
- if submission.submission_comments:
159
- for comment in submission.submission_comments:
160
- comment_text = comment.get('comment', '')
161
- if not comment_text.startswith(codeval_prefix):
162
- continue
163
-
164
- created_at_str = comment.get('created_at', '')
165
- if not created_at_str:
166
- continue
167
-
168
- try:
169
- created_at = datetime.strptime(created_at_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
170
- except ValueError:
171
- continue
172
-
173
- codeval_comments.append({
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
- if show_uncommented and has_codeval and submission.submitted_at:
187
- try:
188
- submitted_at = datetime.strptime(submission.submitted_at, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
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}: {assignment.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}: {assignment.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']):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assignment-codeval
3
- Version: 0.0.23
3
+ Version: 0.0.24
4
4
  Summary: CodEval for evaluating programming assignments
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown