QuizGenerator 0.4.2__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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +627 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1955 -0
- QuizGenerator/generate.py +253 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +579 -0
- QuizGenerator/mixins.py +548 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +391 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
- QuizGenerator/premade_questions/cst334/process.py +648 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
- QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
- QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
- QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
- QuizGenerator/premade_questions/cst463/models/text.py +203 -0
- QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +715 -0
- QuizGenerator/quiz.py +467 -0
- QuizGenerator/regenerate.py +472 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.4.2.dist-info/METADATA +265 -0
- quizgenerator-0.4.2.dist-info/RECORD +52 -0
- quizgenerator-0.4.2.dist-info/WHEEL +4 -0
- quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
- quizgenerator-0.4.2.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
from QuizGenerator.canvas.canvas_interface import CanvasInterface
|
|
11
|
+
|
|
12
|
+
from QuizGenerator.quiz import Quiz
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
log = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
from QuizGenerator.performance import PerformanceTracker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_args():
|
|
21
|
+
parser = argparse.ArgumentParser()
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--env",
|
|
25
|
+
default=os.path.join(Path.home(), '.env'),
|
|
26
|
+
help="Path to .env file specifying canvas details"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument("--debug", action="store_true", help="Set logging level to debug")
|
|
30
|
+
|
|
31
|
+
parser.add_argument("--quiz_yaml", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/exam_generation.yaml"))
|
|
32
|
+
parser.add_argument("--seed", type=int, default=None,
|
|
33
|
+
help="Random seed for quiz generation (default: None for random)")
|
|
34
|
+
|
|
35
|
+
# Canvas flags
|
|
36
|
+
parser.add_argument("--num_canvas", default=0, type=int, help="How many variations of each question to try to upload to canvas.")
|
|
37
|
+
parser.add_argument("--prod", action="store_true")
|
|
38
|
+
parser.add_argument("--course_id", type=int)
|
|
39
|
+
parser.add_argument("--delete-assignment-group", action="store_true",
|
|
40
|
+
help="Delete existing assignment group before uploading new quizzes")
|
|
41
|
+
|
|
42
|
+
# PDF Flags
|
|
43
|
+
parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
|
|
44
|
+
parser.add_argument("--typst", action="store_true",
|
|
45
|
+
help="Use Typst instead of LaTeX for PDF generation")
|
|
46
|
+
|
|
47
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
48
|
+
test_parser = subparsers.add_parser("TEST")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
|
|
53
|
+
if args.num_canvas > 0 and args.course_id is None:
|
|
54
|
+
log.error("Must provide course_id when pushing to canvas")
|
|
55
|
+
exit(8)
|
|
56
|
+
|
|
57
|
+
return args
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test():
|
|
61
|
+
log.info("Running test...")
|
|
62
|
+
|
|
63
|
+
print("\n" + "="*60)
|
|
64
|
+
print("TEST COMPLETE")
|
|
65
|
+
print("="*60)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def generate_latex(latex_text, remove_previous=False):
|
|
69
|
+
|
|
70
|
+
if remove_previous:
|
|
71
|
+
if os.path.exists('out'): shutil.rmtree('out')
|
|
72
|
+
|
|
73
|
+
tmp_tex = tempfile.NamedTemporaryFile('w')
|
|
74
|
+
|
|
75
|
+
tmp_tex.write(latex_text)
|
|
76
|
+
|
|
77
|
+
tmp_tex.flush()
|
|
78
|
+
shutil.copy(f"{tmp_tex.name}", "debug.tex")
|
|
79
|
+
p = subprocess.Popen(
|
|
80
|
+
f"latexmk -pdf -output-directory={os.path.join(os.getcwd(), 'out')} {tmp_tex.name}",
|
|
81
|
+
shell=True,
|
|
82
|
+
stdout=subprocess.PIPE,
|
|
83
|
+
stderr=subprocess.PIPE)
|
|
84
|
+
try:
|
|
85
|
+
p.wait(30)
|
|
86
|
+
except subprocess.TimeoutExpired:
|
|
87
|
+
logging.error("Latex Compile timed out")
|
|
88
|
+
p.kill()
|
|
89
|
+
tmp_tex.close()
|
|
90
|
+
return
|
|
91
|
+
proc = subprocess.Popen(
|
|
92
|
+
f"latexmk -c {tmp_tex.name} -output-directory={os.path.join(os.getcwd(), 'out')}",
|
|
93
|
+
shell=True,
|
|
94
|
+
stdout=subprocess.PIPE,
|
|
95
|
+
stderr=subprocess.PIPE
|
|
96
|
+
)
|
|
97
|
+
proc.wait(timeout=30)
|
|
98
|
+
tmp_tex.close()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_typst(typst_text, remove_previous=False):
|
|
102
|
+
"""
|
|
103
|
+
Generate PDF from Typst source code.
|
|
104
|
+
|
|
105
|
+
Similar to generate_latex, but uses typst compiler instead of latexmk.
|
|
106
|
+
"""
|
|
107
|
+
if remove_previous:
|
|
108
|
+
if os.path.exists('out'):
|
|
109
|
+
shutil.rmtree('out')
|
|
110
|
+
|
|
111
|
+
# Ensure output directory exists
|
|
112
|
+
os.makedirs('out', exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Create temporary Typst file
|
|
115
|
+
tmp_typ = tempfile.NamedTemporaryFile('w', suffix='.typ', delete=False)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
tmp_typ.write(typst_text)
|
|
119
|
+
tmp_typ.flush()
|
|
120
|
+
tmp_typ.close()
|
|
121
|
+
|
|
122
|
+
# Save debug copy
|
|
123
|
+
shutil.copy(tmp_typ.name, "debug.typ")
|
|
124
|
+
|
|
125
|
+
# Compile with typst
|
|
126
|
+
output_pdf = os.path.join(os.getcwd(), 'out', os.path.basename(tmp_typ.name).replace('.typ', '.pdf'))
|
|
127
|
+
|
|
128
|
+
# Use --root to set the filesystem root so absolute paths work correctly
|
|
129
|
+
p = subprocess.Popen(
|
|
130
|
+
['typst', 'compile', '--root', '/', tmp_typ.name, output_pdf],
|
|
131
|
+
stdout=subprocess.PIPE,
|
|
132
|
+
stderr=subprocess.PIPE
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
p.wait(30)
|
|
137
|
+
if p.returncode != 0:
|
|
138
|
+
stderr = p.stderr.read().decode('utf-8')
|
|
139
|
+
log.error(f"Typst compilation failed: {stderr}")
|
|
140
|
+
except subprocess.TimeoutExpired:
|
|
141
|
+
log.error("Typst compile timed out")
|
|
142
|
+
p.kill()
|
|
143
|
+
|
|
144
|
+
finally:
|
|
145
|
+
# Clean up temp file
|
|
146
|
+
if os.path.exists(tmp_typ.name):
|
|
147
|
+
os.unlink(tmp_typ.name)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def generate_quiz(
|
|
151
|
+
path_to_quiz_yaml,
|
|
152
|
+
num_pdfs=0,
|
|
153
|
+
num_canvas=0,
|
|
154
|
+
use_prod=False,
|
|
155
|
+
course_id=None,
|
|
156
|
+
delete_assignment_group=False,
|
|
157
|
+
use_typst=False,
|
|
158
|
+
use_typst_measurement=False,
|
|
159
|
+
base_seed=None
|
|
160
|
+
):
|
|
161
|
+
|
|
162
|
+
quizzes = Quiz.from_yaml(path_to_quiz_yaml)
|
|
163
|
+
|
|
164
|
+
# Handle Canvas uploads with shared assignment group
|
|
165
|
+
if num_canvas > 0:
|
|
166
|
+
canvas_interface = CanvasInterface(prod=use_prod)
|
|
167
|
+
canvas_course = canvas_interface.get_course(course_id=course_id)
|
|
168
|
+
|
|
169
|
+
# Create assignment group once, with delete flag if specified
|
|
170
|
+
assignment_group = canvas_course.create_assignment_group(
|
|
171
|
+
name="dev",
|
|
172
|
+
delete_existing=delete_assignment_group
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
log.info(f"Using assignment group '{assignment_group.name}' for all quizzes")
|
|
176
|
+
|
|
177
|
+
for quiz in quizzes:
|
|
178
|
+
|
|
179
|
+
for i in range(num_pdfs):
|
|
180
|
+
log.debug(f"Generating PDF {i+1}/{num_pdfs}")
|
|
181
|
+
# If base_seed is provided, use it with an offset for each PDF
|
|
182
|
+
# Otherwise generate a random seed for this PDF
|
|
183
|
+
if base_seed is not None:
|
|
184
|
+
pdf_seed = base_seed + (i * 1000) # Large gap to avoid overlap with rng_seed_offset
|
|
185
|
+
else:
|
|
186
|
+
pdf_seed = random.randint(0, 1_000_000)
|
|
187
|
+
|
|
188
|
+
log.info(f"Generating PDF {i+1} with seed: {pdf_seed}")
|
|
189
|
+
|
|
190
|
+
if use_typst:
|
|
191
|
+
# Generate using Typst
|
|
192
|
+
typst_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render("typst")
|
|
193
|
+
generate_typst(typst_text, remove_previous=(i==0))
|
|
194
|
+
else:
|
|
195
|
+
# Generate using LaTeX (default)
|
|
196
|
+
latex_text = quiz.get_quiz(rng_seed=pdf_seed, use_typst_measurement=use_typst_measurement).render_latex()
|
|
197
|
+
generate_latex(latex_text, remove_previous=(i==0))
|
|
198
|
+
|
|
199
|
+
if num_canvas > 0:
|
|
200
|
+
canvas_course.push_quiz_to_canvas(
|
|
201
|
+
quiz,
|
|
202
|
+
num_canvas,
|
|
203
|
+
title=quiz.name,
|
|
204
|
+
is_practice=quiz.practice,
|
|
205
|
+
assignment_group=assignment_group
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
quiz.describe()
|
|
209
|
+
|
|
210
|
+
def main():
|
|
211
|
+
|
|
212
|
+
args = parse_args()
|
|
213
|
+
|
|
214
|
+
# Load environment variables
|
|
215
|
+
load_dotenv(args.env)
|
|
216
|
+
|
|
217
|
+
if args.debug:
|
|
218
|
+
# Set root logger to DEBUG
|
|
219
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
220
|
+
|
|
221
|
+
# Set all handlers to DEBUG level
|
|
222
|
+
for handler in logging.getLogger().handlers:
|
|
223
|
+
handler.setLevel(logging.DEBUG)
|
|
224
|
+
|
|
225
|
+
# Set named loggers to DEBUG
|
|
226
|
+
for logger_name in ['QuizGenerator', 'lms_interface', '__main__']:
|
|
227
|
+
logger = logging.getLogger(logger_name)
|
|
228
|
+
logger.setLevel(logging.DEBUG)
|
|
229
|
+
for handler in logger.handlers:
|
|
230
|
+
handler.setLevel(logging.DEBUG)
|
|
231
|
+
|
|
232
|
+
if args.command == "TEST":
|
|
233
|
+
test()
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Clear any previous metrics
|
|
237
|
+
PerformanceTracker.clear_metrics()
|
|
238
|
+
|
|
239
|
+
generate_quiz(
|
|
240
|
+
args.quiz_yaml,
|
|
241
|
+
num_pdfs=args.num_pdfs,
|
|
242
|
+
num_canvas=args.num_canvas,
|
|
243
|
+
use_prod=args.prod,
|
|
244
|
+
course_id=args.course_id,
|
|
245
|
+
delete_assignment_group=getattr(args, 'delete_assignment_group', False),
|
|
246
|
+
use_typst=getattr(args, 'typst', False),
|
|
247
|
+
use_typst_measurement=getattr(args, 'typst_measurement', False),
|
|
248
|
+
base_seed=getattr(args, 'seed', None)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
disable_existing_loggers: false
|
|
3
|
+
|
|
4
|
+
formatters:
|
|
5
|
+
standard:
|
|
6
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
7
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
8
|
+
|
|
9
|
+
detailed:
|
|
10
|
+
format: '%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(funcName)s:%(lineno)d - %(message)s'
|
|
11
|
+
datefmt: '%Y-%m-%d %H:%M:%S'
|
|
12
|
+
|
|
13
|
+
simple:
|
|
14
|
+
format: '%(levelname)s - %(message)s'
|
|
15
|
+
|
|
16
|
+
handlers:
|
|
17
|
+
console:
|
|
18
|
+
class: logging.StreamHandler
|
|
19
|
+
level: INFO
|
|
20
|
+
formatter: standard
|
|
21
|
+
stream: ext://sys.stdout
|
|
22
|
+
|
|
23
|
+
file:
|
|
24
|
+
class: logging.FileHandler
|
|
25
|
+
level: INFO
|
|
26
|
+
formatter: detailed
|
|
27
|
+
filename: ${LOG_FILE:-teachingtools.log}
|
|
28
|
+
mode: a
|
|
29
|
+
|
|
30
|
+
error_file:
|
|
31
|
+
class: logging.FileHandler
|
|
32
|
+
level: ERROR
|
|
33
|
+
formatter: detailed
|
|
34
|
+
filename: ${ERROR_LOG_FILE:-teachingtools_errors.log}
|
|
35
|
+
mode: a
|
|
36
|
+
|
|
37
|
+
loggers:
|
|
38
|
+
QuizGenerator:
|
|
39
|
+
level: INFO
|
|
40
|
+
handlers: [console, file]
|
|
41
|
+
propagate: false
|
|
42
|
+
|
|
43
|
+
lms_interface:
|
|
44
|
+
level: INFO
|
|
45
|
+
handlers: [console, file]
|
|
46
|
+
propagate: false
|
|
47
|
+
|
|
48
|
+
canvasapi:
|
|
49
|
+
level: WARNING
|
|
50
|
+
handlers: [console]
|
|
51
|
+
propagate: false
|
|
52
|
+
|
|
53
|
+
root:
|
|
54
|
+
level: INFO
|
|
55
|
+
handlers: [console, file, error_file]
|