assignment-codeval 0.0.30__py3-none-any.whl → 0.0.32__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.
@@ -75,8 +75,10 @@ def extract_assignment_from_codeval(codeval_path: str) -> tuple[str, str, str]:
75
75
  with open(codeval_path, "r", encoding="utf-8") as f:
76
76
  content = f.read()
77
77
 
78
- # Extract content between CRT_HW START and CRT_HW END
79
- match = re.search(r"CRT_HW START \S+\n(.*?)CRT_HW END", content, re.DOTALL)
78
+ # Extract content between ASSIGNMENT START / CRT_HW START and ASSIGNMENT END / CRT_HW END.
79
+ # The title after the marker may contain spaces (e.g. "CRT_HW START Place Boats")
80
+ # or be absent entirely, so match the rest of the marker line, not a single token.
81
+ match = re.search(r"(?:ASSIGNMENT START|CRT_HW START)[^\n]*\n(.*?)(?:ASSIGNMENT END|CRT_HW END)", content, re.DOTALL)
80
82
  if match:
81
83
  description = match.group(1).strip()
82
84
  else:
@@ -223,10 +225,17 @@ def call_openai(model_id: str, prompt: str, api_key: str) -> Optional[str]:
223
225
  try:
224
226
  client = openai.OpenAI(api_key=api_key)
225
227
 
228
+ # Reasoning models (o1/o3/o4 families, gpt-5) reject `max_tokens` and
229
+ # require `max_completion_tokens` instead.
230
+ if model_id.startswith(("o1", "o3", "o4", "gpt-5")):
231
+ token_kwargs = {"max_completion_tokens": 8192}
232
+ else:
233
+ token_kwargs = {"max_tokens": 8192}
234
+
226
235
  response = client.chat.completions.create(
227
236
  model=model_id,
228
237
  messages=[{"role": "user", "content": prompt}],
229
- max_tokens=8192,
238
+ **token_kwargs,
230
239
  )
231
240
 
232
241
  return response.choices[0].message.content
@@ -1,180 +1,192 @@
1
- import os
2
- import re
3
- import zipfile
4
-
5
- import markdown
6
-
7
- from assignment_codeval.commons import info, get_config
8
-
9
-
10
- _ANSI_STYLES = {
11
- '1': 'font-weight:bold',
12
- '4': 'text-decoration:underline',
13
- '30': 'color:black', '31': 'color:red', '32': 'color:green',
14
- '33': 'color:#a50', '34': 'color:blue', '35': 'color:magenta',
15
- '36': 'color:cyan', '37': 'color:white',
16
- '40': 'background-color:black', '41': 'background-color:red',
17
- '42': 'background-color:green', '43': 'background-color:yellow',
18
- '44': 'background-color:blue', '45': 'background-color:magenta',
19
- '46': 'background-color:cyan', '47': 'background-color:white',
20
- }
21
-
22
-
23
- def ansi_to_html(text):
24
- """Convert ANSI escape codes in text to HTML span tags."""
25
- parts = re.split(r'\x1b\[([\d;]*)m', text)
26
- result = []
27
- open_spans = 0
28
- for i, part in enumerate(parts):
29
- if i % 2 == 0:
30
- result.append(part)
31
- else:
32
- codes = part.split(';') if part else ['0']
33
- if '0' in codes:
34
- result.append('</span>' * open_spans)
35
- open_spans = 0
36
- else:
37
- styles = [_ANSI_STYLES[c] for c in codes if c in _ANSI_STYLES]
38
- if styles:
39
- result.append(f'<span style="{";".join(styles)}">')
40
- open_spans += 1
41
- result.append('</span>' * open_spans)
42
- return ''.join(result)
43
-
44
-
45
- def _read_file_content(filename, spec_dir, zip_archives=None):
46
- """Read the content of a file referenced by an IF/OF/EF tag.
47
-
48
- Searches local spec_dir first, then zip archives. Returns content as string.
49
- """
50
- if spec_dir:
51
- filepath = os.path.join(spec_dir, filename)
52
- try:
53
- with open(filepath, 'r') as f:
54
- return f.read()
55
- except (FileNotFoundError, OSError):
56
- pass
57
- if zip_archives:
58
- for zf in zip_archives:
59
- try:
60
- return zf.read(filename).decode('utf-8')
61
- except KeyError:
62
- continue
63
- return None
64
-
65
-
66
- def sampleTestCases(listOfTC, numOfTC, spec_dir=None, zip_archives=None):
67
- counter = 0
68
- samples = "<pre><code>"
69
- for line in listOfTC:
70
- if line.startswith('T ', 0, 2):
71
- counter = counter + 1
72
- if counter > numOfTC:
73
- break
74
- samples = samples + "\n" + "Command to RUN: " + line[2:]
75
- elif line.startswith('IF '):
76
- filename = line[3:].strip()
77
- content = _read_file_content(filename, spec_dir, zip_archives)
78
- if content is not None:
79
- samples = samples + "<span style=\"color:green\">" + ansi_to_html(content) + "</span>"
80
- else:
81
- samples = samples + "<span style=\"color:green\">Input from file: " + filename + "\n</span>"
82
- elif line.startswith('IB '):
83
- samples = samples + "<span style=\"color:green\">" + line[3:] + "</span>"
84
- elif line.startswith('I '):
85
- samples = samples + "<span style=\"color:green\">" + line[2:] + "</span>"
86
- elif line.startswith('OF '):
87
- filename = line[3:].strip()
88
- content = _read_file_content(filename, spec_dir, zip_archives)
89
- if content is not None:
90
- samples = samples + "<span style=\"color:blue\">" + ansi_to_html(content) + "</span>"
91
- else:
92
- samples = samples + "<span style=\"color:blue\">Expected output from file: " + filename + "\n</span>"
93
- elif line.startswith('OB '):
94
- samples = samples + "<span style=\"color:blue\">" + line[3:] + "</span>"
95
- elif line.startswith('O '):
96
- samples = samples + "<span style=\"color:blue\">" + line[2:] + "</span>"
97
- elif line.startswith('X '):
98
- samples = samples + "Expected Exit Code: " + line[2:]
99
- elif line.startswith('EF '):
100
- filename = line[3:].strip()
101
- content = _read_file_content(filename, spec_dir, zip_archives)
102
- if content is not None:
103
- samples = samples + "<span style=\"color:Tomato\">" + ansi_to_html(content) + "</span>"
104
- else:
105
- samples = samples + "<span style=\"color:Tomato\">Expected error from file: " + filename + "\n</span>"
106
- elif line.startswith('EB '):
107
- samples = samples + "<span style=\"color:Tomato\">" + line[3:] + "</span>"
108
- elif line.startswith('E '):
109
- samples = samples + "<span style=\"color:Tomato\">" + line[2:] + "</span>"
110
- else:
111
- continue
112
- samples = samples + "</code></pre>"
113
- return samples
114
-
115
-
116
- def mdToHtml(file_name, files_resolver=None):
117
- with open(file_name, 'r') as f:
118
- text = ""
119
- examples = []
120
- assignment = ""
121
- compile_command = ""
122
- past_crt_hw = False
123
- numOfSampleTC = 1
124
- zip_paths = []
125
- for line in f:
126
- if 'CRT_HW START' in line:
127
- assignment_name = line[13:].strip()
128
- elif 'CRT_HW END' in line:
129
- assignment = text
130
- past_crt_hw = True
131
- elif line.startswith(('T ', 'I ', 'IB ', 'IF ', 'O ', 'OB ', 'OF ',
132
- 'EF ', 'X ', 'E ', 'EB ')):
133
- examples.append(line)
134
- elif line.startswith('HT '):
135
- break
136
- else:
137
- if past_crt_hw and line.startswith('C '):
138
- compile_command = line[2:].strip()
139
- if past_crt_hw and line.startswith('Z '):
140
- zip_paths.append(line[2:].strip())
141
- if 'EXMPLS ' in line:
142
- numOfSampleTC = int(line[7:])
143
- text = text + line
144
- spec_dir = os.path.dirname(os.path.abspath(file_name))
145
-
146
- # Open zip archives for IF/OF/EF file resolution
147
- zip_archives = []
148
- try:
149
- for zp in zip_paths:
150
- full_path = os.path.join(spec_dir, zp)
151
- if os.path.exists(full_path):
152
- zip_archives.append(zipfile.ZipFile(full_path, 'r'))
153
- samples = sampleTestCases(examples, numOfSampleTC, spec_dir,
154
- zip_archives)
155
- finally:
156
- for zf in zip_archives:
157
- zf.close()
158
-
159
- assignment = re.sub('EXMPLS [0-9]+', samples, assignment)
160
- if compile_command:
161
- assignment = assignment.replace('COMPILE', compile_command)
162
- # Handle FILE macros in two passes:
163
- # 1. FILE inside markdown link syntax: [text](FILE[name]) -> [text](url)
164
- assignment = re.sub(r'\]\(FILE\[([^]]+)]\)',
165
- lambda m: f']({files_resolver(m.group(1))})' if files_resolver else f'](FILE[{m.group(1)}])',
166
- assignment)
167
- # 2. Standalone FILE macros: FILE[name] -> [name](url)
168
- assignment = re.sub(r'FILE\[([^]]+)]',
169
- lambda m: f'[{m.group(1)}]({files_resolver(m.group(1))})' if files_resolver else f'FILE[{m.group(1)}]',
170
- assignment)
171
- html = markdown.markdown(assignment,
172
- extensions=['mdx_better_lists', 'extra'],
173
- extension_configs={'mdx_better_lists': {'split_paragraph_lists': True}})
174
-
175
- if get_config().dry_run:
176
- html_file_name = file_name + '.html'
177
- with open(html_file_name, 'w') as f:
178
- f.write(html)
179
- info(f'HTML preview created in the path : {html_file_name}')
180
- return assignment_name, html
1
+ import os
2
+ import re
3
+ import zipfile
4
+ from html import escape as html_escape
5
+
6
+ import markdown
7
+
8
+ from assignment_codeval.commons import info, get_config
9
+
10
+
11
+ _ANSI_STYLES = {
12
+ '1': 'font-weight:bold',
13
+ '4': 'text-decoration:underline',
14
+ '30': 'color:black', '31': 'color:red', '32': 'color:green',
15
+ '33': 'color:#a50', '34': 'color:blue', '35': 'color:magenta',
16
+ '36': 'color:cyan', '37': 'color:white',
17
+ '40': 'background-color:black', '41': 'background-color:red',
18
+ '42': 'background-color:green', '43': 'background-color:yellow',
19
+ '44': 'background-color:blue', '45': 'background-color:magenta',
20
+ '46': 'background-color:cyan', '47': 'background-color:white',
21
+ }
22
+
23
+
24
+ def ansi_to_html(text):
25
+ """Convert ANSI escape codes in text to HTML span tags.
26
+
27
+ The text parts are HTML-escaped so characters like < and & render
28
+ literally instead of being parsed as markup.
29
+ """
30
+ parts = re.split(r'\x1b\[([\d;]*)m', text)
31
+ result = []
32
+ open_spans = 0
33
+ for i, part in enumerate(parts):
34
+ if i % 2 == 0:
35
+ result.append(html_escape(part, quote=False))
36
+ else:
37
+ codes = part.split(';') if part else ['0']
38
+ if '0' in codes:
39
+ result.append('</span>' * open_spans)
40
+ open_spans = 0
41
+ else:
42
+ styles = [_ANSI_STYLES[c] for c in codes if c in _ANSI_STYLES]
43
+ if styles:
44
+ result.append(f'<span style="{";".join(styles)}">')
45
+ open_spans += 1
46
+ result.append('</span>' * open_spans)
47
+ return ''.join(result)
48
+
49
+
50
+ def _read_file_content(filename, spec_dir, zip_archives=None):
51
+ """Read the content of a file referenced by an IF/OF/EF tag.
52
+
53
+ Searches local spec_dir first, then zip archives. Returns content as string.
54
+ """
55
+ if spec_dir:
56
+ filepath = os.path.join(spec_dir, filename)
57
+ try:
58
+ with open(filepath, 'r') as f:
59
+ return f.read()
60
+ except (FileNotFoundError, OSError):
61
+ pass
62
+ if zip_archives:
63
+ for zf in zip_archives:
64
+ try:
65
+ return zf.read(filename).decode('utf-8')
66
+ except KeyError:
67
+ continue
68
+ return None
69
+
70
+
71
+ def sampleTestCases(listOfTC, numOfTC, spec_dir=None, zip_archives=None):
72
+ counter = 0
73
+ samples = "<pre><code>"
74
+ esc = lambda s: html_escape(s, quote=False)
75
+ for line in listOfTC:
76
+ if line.startswith('T ', 0, 2):
77
+ counter = counter + 1
78
+ if counter > numOfTC:
79
+ break
80
+ samples = samples + "\n" + "Command to RUN: " + esc(line[2:])
81
+ elif line.startswith('IF '):
82
+ filename = line[3:].strip()
83
+ content = _read_file_content(filename, spec_dir, zip_archives)
84
+ if content is not None:
85
+ samples = samples + "<span style=\"color:green\">" + ansi_to_html(content) + "</span>"
86
+ else:
87
+ samples = samples + "<span style=\"color:green\">Input from file: " + esc(filename) + "\n</span>"
88
+ elif line.startswith('IB '):
89
+ samples = samples + "<span style=\"color:green\">" + esc(line[3:]) + "</span>"
90
+ elif line.startswith('I '):
91
+ samples = samples + "<span style=\"color:green\">" + esc(line[2:]) + "</span>"
92
+ elif line.startswith('OF '):
93
+ filename = line[3:].strip()
94
+ content = _read_file_content(filename, spec_dir, zip_archives)
95
+ if content is not None:
96
+ samples = samples + "<span style=\"color:blue\">" + ansi_to_html(content) + "</span>"
97
+ else:
98
+ samples = samples + "<span style=\"color:blue\">Expected output from file: " + esc(filename) + "\n</span>"
99
+ elif line.startswith('OB '):
100
+ samples = samples + "<span style=\"color:blue\">" + esc(line[3:]) + "</span>"
101
+ elif line.startswith('O '):
102
+ samples = samples + "<span style=\"color:blue\">" + esc(line[2:]) + "</span>"
103
+ elif line.startswith('X '):
104
+ samples = samples + "Expected Exit Code: " + esc(line[2:])
105
+ elif line.startswith('EF '):
106
+ filename = line[3:].strip()
107
+ content = _read_file_content(filename, spec_dir, zip_archives)
108
+ if content is not None:
109
+ samples = samples + "<span style=\"color:Tomato\">" + ansi_to_html(content) + "</span>"
110
+ else:
111
+ samples = samples + "<span style=\"color:Tomato\">Expected error from file: " + esc(filename) + "\n</span>"
112
+ elif line.startswith('EB '):
113
+ samples = samples + "<span style=\"color:Tomato\">" + esc(line[3:]) + "</span>"
114
+ elif line.startswith('E '):
115
+ samples = samples + "<span style=\"color:Tomato\">" + esc(line[2:]) + "</span>"
116
+ else:
117
+ continue
118
+ samples = samples + "</code></pre>"
119
+ return samples
120
+
121
+
122
+ def mdToHtml(file_name, files_resolver=None):
123
+ with open(file_name, 'r') as f:
124
+ text = ""
125
+ examples = []
126
+ assignment = ""
127
+ compile_command = ""
128
+ past_crt_hw = False
129
+ numOfSampleTC = 1
130
+ zip_paths = []
131
+ for line in f:
132
+ if 'ASSIGNMENT START' in line:
133
+ assignment_name = line[len('ASSIGNMENT START'):].strip()
134
+ elif 'CRT_HW START' in line:
135
+ assignment_name = line[len('CRT_HW START'):].strip()
136
+ elif 'ASSIGNMENT END' in line or 'CRT_HW END' in line:
137
+ assignment = text
138
+ past_crt_hw = True
139
+ elif line.startswith(('T ', 'I ', 'IB ', 'IF ', 'O ', 'OB ', 'OF ',
140
+ 'EF ', 'X ', 'E ', 'EB ')):
141
+ examples.append(line)
142
+ elif line.startswith('HT '):
143
+ break
144
+ else:
145
+ if past_crt_hw and line.startswith('C '):
146
+ compile_command = line[2:].strip()
147
+ if past_crt_hw and line.startswith('Z '):
148
+ zip_paths.append(line[2:].strip())
149
+ exmpls_match = re.search(r'EXMPLS\s+([0-9]+)', line)
150
+ if exmpls_match:
151
+ numOfSampleTC = int(exmpls_match.group(1))
152
+ text = text + line
153
+ spec_dir = os.path.dirname(os.path.abspath(file_name))
154
+
155
+ # Open zip archives for IF/OF/EF file resolution
156
+ zip_archives = []
157
+ try:
158
+ for zp in zip_paths:
159
+ full_path = os.path.join(spec_dir, zp)
160
+ if os.path.exists(full_path):
161
+ zip_archives.append(zipfile.ZipFile(full_path, 'r'))
162
+ samples = sampleTestCases(examples, numOfSampleTC, spec_dir,
163
+ zip_archives)
164
+ finally:
165
+ for zf in zip_archives:
166
+ zf.close()
167
+
168
+ # Use a function replacement so backslash sequences in the sample data
169
+ # (e.g. "\d" or "\1" in expected output) are inserted literally rather
170
+ # than interpreted as regex escapes.
171
+ assignment = re.sub('EXMPLS [0-9]+', lambda m: samples, assignment)
172
+ if compile_command:
173
+ assignment = assignment.replace('COMPILE', compile_command)
174
+ # Handle FILE macros in two passes:
175
+ # 1. FILE inside markdown link syntax: [text](FILE[name]) -> [text](url)
176
+ assignment = re.sub(r'\]\(FILE\[([^]]+)]\)',
177
+ lambda m: f']({files_resolver(m.group(1))})' if files_resolver else f'](FILE[{m.group(1)}])',
178
+ assignment)
179
+ # 2. Standalone FILE macros: FILE[name] -> [name](url)
180
+ assignment = re.sub(r'FILE\[([^]]+)]',
181
+ lambda m: f'[{m.group(1)}]({files_resolver(m.group(1))})' if files_resolver else f'FILE[{m.group(1)}]',
182
+ assignment)
183
+ html = markdown.markdown(assignment,
184
+ extensions=['mdx_better_lists', 'extra'],
185
+ extension_configs={'mdx_better_lists': {'split_paragraph_lists': True}})
186
+
187
+ if get_config().dry_run:
188
+ html_file_name = file_name + '.html'
189
+ with open(html_file_name, 'w') as f:
190
+ f.write(html)
191
+ info(f'HTML preview created in the path : {html_file_name}')
192
+ return assignment_name, html
@@ -38,12 +38,14 @@ def get_assignment_subfolder(course, codeval_folder, assignment_name):
38
38
 
39
39
 
40
40
  def extract_assignment_name(specname):
41
- """Extract the assignment name from the CRT_HW START line of a codeval spec file."""
41
+ """Extract the assignment name from the ASSIGNMENT START (or CRT_HW START) line of a codeval spec file."""
42
42
  with open(specname, 'r') as f:
43
43
  for line in f:
44
+ if 'ASSIGNMENT START' in line:
45
+ return line[len('ASSIGNMENT START'):].strip()
44
46
  if 'CRT_HW START' in line:
45
- return line[13:].strip()
46
- error(f"could not find CRT_HW START in {specname}")
47
+ return line[len('CRT_HW START'):].strip()
48
+ error(f"could not find ASSIGNMENT START in {specname}")
47
49
  exit(2)
48
50
 
49
51
 
@@ -53,9 +55,9 @@ def extract_file_macros(specname):
53
55
  in_assignment = False
54
56
  with open(specname, 'r') as f:
55
57
  for line in f:
56
- if 'CRT_HW START' in line:
58
+ if 'ASSIGNMENT START' in line or 'CRT_HW START' in line:
57
59
  in_assignment = True
58
- elif 'CRT_HW END' in line:
60
+ elif 'ASSIGNMENT END' in line or 'CRT_HW END' in line:
59
61
  in_assignment = False
60
62
  elif in_assignment:
61
63
  # Find all FILE[filename] patterns in this line
@@ -93,7 +95,7 @@ def files_resolver(filename):
93
95
  if not matches:
94
96
  warn(f"could not find {filename} in {z}")
95
97
  elif len(matches) > 1:
96
- warn("found multiple matches for {filename} in {z}: {matches}")
98
+ warn(f"found multiple matches for {filename} in {z}: {matches}")
97
99
  if matches:
98
100
  zfname = matches[0]
99
101
  with TemporaryDirectory("ce") as tmpdir:
@@ -113,7 +115,9 @@ def files_resolver(filename):
113
115
  @click.option("--verbose/--no-verbose", default=False, show_default=True, help="Verbose actions")
114
116
  @click.option("--group_name", default="Assignments", show_default=True,
115
117
  help="Group name in which assignments needs to be created.")
116
- def create_assignment(dryrun, verbose, course_name, group_name, specname, extra):
118
+ @click.option("--active/--inactive", default=True, show_default=True,
119
+ help="Only look at active courses.")
120
+ def create_assignment(dryrun, verbose, course_name, group_name, specname, extra, active):
117
121
  """
118
122
  Create the assignment in the given course.
119
123
  """
@@ -126,7 +130,7 @@ def create_assignment(dryrun, verbose, course_name, group_name, specname, extra)
126
130
  (canvas, user) = connect_to_canvas()
127
131
 
128
132
  try:
129
- course = get_course(canvas, course_name)
133
+ course = get_course(canvas, course_name, is_active=active)
130
134
  except Exception as e:
131
135
  errorWithException(f'get_course api failed with following error : {e}')
132
136
  else:
@@ -150,16 +154,17 @@ def create_assignment(dryrun, verbose, course_name, group_name, specname, extra)
150
154
  canvas_folder = get_assignment_subfolder(course, codeval_folder, assign_name_early)
151
155
  if extra:
152
156
  upload_assignment_files(canvas_folder, extra)
153
- # find zipfiles in spec
157
+ # find zipfiles in spec; resolve them relative to the spec directory so the
158
+ # command works when run from any working directory
159
+ spec_dir = os.path.dirname(os.path.abspath(specname))
154
160
  with open(specname) as f:
155
161
  for line in f:
156
162
  if line.startswith("Z "):
157
- zipfile = line[2:].strip()
158
- if zipfile not in zip_files:
159
- zip_files.append(zipfile)
163
+ zip_name = os.path.join(spec_dir, line[2:].strip())
164
+ if zip_name not in zip_files:
165
+ zip_files.append(zip_name)
160
166
 
161
167
  # Extract FILE macros and upload local files that exist
162
- spec_dir = os.path.dirname(os.path.abspath(specname))
163
168
  file_macros = extract_file_macros(specname)
164
169
  for filename in file_macros:
165
170
  if filename not in file_dict: