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.
Files changed (52) hide show
  1. QuizGenerator/README.md +5 -0
  2. QuizGenerator/__init__.py +27 -0
  3. QuizGenerator/__main__.py +7 -0
  4. QuizGenerator/canvas/__init__.py +13 -0
  5. QuizGenerator/canvas/canvas_interface.py +627 -0
  6. QuizGenerator/canvas/classes.py +235 -0
  7. QuizGenerator/constants.py +149 -0
  8. QuizGenerator/contentast.py +1955 -0
  9. QuizGenerator/generate.py +253 -0
  10. QuizGenerator/logging.yaml +55 -0
  11. QuizGenerator/misc.py +579 -0
  12. QuizGenerator/mixins.py +548 -0
  13. QuizGenerator/performance.py +202 -0
  14. QuizGenerator/premade_questions/__init__.py +0 -0
  15. QuizGenerator/premade_questions/basic.py +103 -0
  16. QuizGenerator/premade_questions/cst334/__init__.py +1 -0
  17. QuizGenerator/premade_questions/cst334/languages.py +391 -0
  18. QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
  19. QuizGenerator/premade_questions/cst334/memory_questions.py +1400 -0
  20. QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
  21. QuizGenerator/premade_questions/cst334/persistence_questions.py +451 -0
  22. QuizGenerator/premade_questions/cst334/process.py +648 -0
  23. QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  24. QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
  25. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
  26. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
  27. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
  28. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
  29. QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
  30. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
  31. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
  32. QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  33. QuizGenerator/premade_questions/cst463/models/attention.py +192 -0
  34. QuizGenerator/premade_questions/cst463/models/cnns.py +186 -0
  35. QuizGenerator/premade_questions/cst463/models/matrices.py +24 -0
  36. QuizGenerator/premade_questions/cst463/models/rnns.py +202 -0
  37. QuizGenerator/premade_questions/cst463/models/text.py +203 -0
  38. QuizGenerator/premade_questions/cst463/models/weight_counting.py +227 -0
  39. QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
  40. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1314 -0
  41. QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
  42. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
  43. QuizGenerator/qrcode_generator.py +293 -0
  44. QuizGenerator/question.py +715 -0
  45. QuizGenerator/quiz.py +467 -0
  46. QuizGenerator/regenerate.py +472 -0
  47. QuizGenerator/typst_utils.py +113 -0
  48. quizgenerator-0.4.2.dist-info/METADATA +265 -0
  49. quizgenerator-0.4.2.dist-info/RECORD +52 -0
  50. quizgenerator-0.4.2.dist-info/WHEEL +4 -0
  51. quizgenerator-0.4.2.dist-info/entry_points.txt +3 -0
  52. 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]