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,548 @@
1
+ #!env python
2
+ """
3
+ Mixin classes to reduce boilerplate in question generation.
4
+ These mixins provide reusable patterns for common question structures.
5
+ """
6
+
7
+ import abc
8
+ from typing import Dict, List, Any, Union
9
+ from QuizGenerator.misc import Answer
10
+ from QuizGenerator.contentast import ContentAST
11
+
12
+
13
+ class TableQuestionMixin:
14
+ """
15
+ Mixin providing common table generation patterns for questions.
16
+
17
+ This mixin identifies and abstracts the most common table patterns used
18
+ across question types, reducing repetitive ContentAST.Table creation code.
19
+ """
20
+
21
+ def create_info_table(self, info_dict: Dict[str, Any], transpose: bool = False) -> ContentAST.Table:
22
+ """
23
+ Creates a vertical info table (key-value pairs).
24
+
25
+ Common pattern: Display parameters/givens in a clean table format.
26
+ Used by: HardDriveAccessTime, BaseAndBounds, etc.
27
+
28
+ Args:
29
+ info_dict: Dictionary of {label: value} pairs
30
+ transpose: Whether to transpose the table (default: False)
31
+
32
+ Returns:
33
+ ContentAST.Table with the information formatted
34
+ """
35
+ # Don't convert ContentAST elements to strings - let them render properly
36
+ table_data = []
37
+ for key, value in info_dict.items():
38
+ # Keep ContentAST elements as-is, convert others to strings
39
+ if isinstance(value, ContentAST.Element):
40
+ table_data.append([key, value])
41
+ else:
42
+ table_data.append([key, str(value)])
43
+
44
+ return ContentAST.Table(
45
+ data=table_data,
46
+ transpose=transpose
47
+ )
48
+
49
+ def create_answer_table(
50
+ self,
51
+ headers: List[str],
52
+ data_rows: List[Dict[str, Any]],
53
+ answer_columns: List[str] = None
54
+ ) -> ContentAST.Table:
55
+ """
56
+ Creates a table where some cells are answer blanks.
57
+
58
+ Common pattern: Mix of given data and answer blanks in a structured table.
59
+ Used by: VirtualAddressParts, SchedulingQuestion, CachingQuestion, etc.
60
+
61
+ Args:
62
+ headers: Column headers for the table
63
+ data_rows: List of dictionaries, each representing a row
64
+ answer_columns: List of column names that should be treated as answers
65
+
66
+ Returns:
67
+ ContentAST.Table with answers embedded in appropriate cells
68
+ """
69
+ answer_columns = answer_columns or []
70
+
71
+ def format_cell(row_data: Dict, column: str) -> Union[str, ContentAST.Answer]:
72
+ """Format a cell based on whether it should be an answer or plain data"""
73
+ value = row_data.get(column, "")
74
+
75
+ # If this column should contain answers and the value is an Answer object
76
+ if column in answer_columns and isinstance(value, Answer):
77
+ return ContentAST.Answer(value)
78
+ # If this column should contain answers but we have the answer key
79
+ elif column in answer_columns and isinstance(value, str) and hasattr(self, 'answers'):
80
+ answer_obj = self.answers.get(value)
81
+ if answer_obj:
82
+ return ContentAST.Answer(answer_obj)
83
+
84
+ # Otherwise return as plain data
85
+ return str(value)
86
+
87
+ table_data = [
88
+ [format_cell(row, header) for header in headers]
89
+ for row in data_rows
90
+ ]
91
+
92
+ return ContentAST.Table(
93
+ headers=headers,
94
+ data=table_data
95
+ )
96
+
97
+ def create_parameter_answer_table(
98
+ self,
99
+ parameter_info: Dict[str, Any],
100
+ answer_label: str,
101
+ answer_key: str,
102
+ transpose: bool = True
103
+ ) -> ContentAST.Table:
104
+ """
105
+ Creates a table combining parameters with a single answer.
106
+
107
+ Common pattern: Show parameters/context, then ask for one calculated result.
108
+ Used by: BaseAndBounds, many memory questions, etc.
109
+
110
+ Args:
111
+ parameter_info: Dictionary of {parameter_name: value}
112
+ answer_label: Label for the answer row
113
+ answer_key: Key to look up the answer in self.answers
114
+ transpose: Whether to show as vertical table (default: True)
115
+
116
+ Returns:
117
+ ContentAST.Table with parameters and answer
118
+ """
119
+ # Build data with parameters plus answer row
120
+ data = [[key, str(value)] for key, value in parameter_info.items()]
121
+
122
+ # Add answer row
123
+ if hasattr(self, 'answers') and answer_key in self.answers:
124
+ data.append([answer_label, ContentAST.Answer(self.answers[answer_key])])
125
+ else:
126
+ data.append([answer_label, f"[{answer_key}]"]) # Fallback
127
+
128
+ return ContentAST.Table(
129
+ data=data,
130
+ transpose=transpose
131
+ )
132
+
133
+ def create_fill_in_table(
134
+ self,
135
+ headers: List[str],
136
+ template_rows: List[Dict[str, Any]]
137
+ ) -> ContentAST.Table:
138
+ """
139
+ Creates a table where multiple cells are answer blanks to fill in.
140
+
141
+ Common pattern: Show a partially completed table where students fill blanks.
142
+ Used by: CachingQuestion, SchedulingQuestion, etc.
143
+
144
+ Args:
145
+ headers: Column headers
146
+ template_rows: Rows where values can be data or answer keys
147
+
148
+ Returns:
149
+ ContentAST.Table with multiple answer blanks
150
+ """
151
+
152
+ def process_cell_value(value: Any) -> Union[str, ContentAST.Answer]:
153
+ """Convert cell values to appropriate display format"""
154
+ # If it's already an Answer object, wrap it
155
+ if isinstance(value, Answer):
156
+ return ContentAST.Answer(value)
157
+ # If it's a string that looks like an answer key, try to resolve it
158
+ elif isinstance(value, str) and value.startswith("answer__") and hasattr(self, 'answers'):
159
+ answer_obj = self.answers.get(value)
160
+ if answer_obj:
161
+ return ContentAST.Answer(answer_obj)
162
+ # Otherwise return as-is
163
+ return str(value)
164
+
165
+ table_data = [
166
+ [process_cell_value(row.get(header, "")) for header in headers]
167
+ for row in template_rows
168
+ ]
169
+
170
+ return ContentAST.Table(
171
+ headers=headers,
172
+ data=table_data
173
+ )
174
+
175
+
176
+ class BodyTemplatesMixin:
177
+ """
178
+ Mixin providing common body structure patterns.
179
+
180
+ These methods create complete ContentAST.Section objects following
181
+ common question layout patterns.
182
+ """
183
+
184
+ def create_calculation_with_info_body(
185
+ self,
186
+ intro_text: str,
187
+ info_table: ContentAST.Table,
188
+ answer_block: ContentAST.AnswerBlock
189
+ ) -> ContentAST.Section:
190
+ """
191
+ Standard pattern: intro text + info table + answer block.
192
+
193
+ Used by: HardDriveAccessTime, AverageMemoryAccessTime, etc.
194
+ """
195
+ body = ContentAST.Section()
196
+ body.add_element(ContentAST.Paragraph([intro_text]))
197
+ body.add_element(info_table)
198
+ body.add_element(answer_block)
199
+ return body
200
+
201
+ def create_fill_in_table_body(
202
+ self,
203
+ intro_text: str,
204
+ instructions: str,
205
+ table: ContentAST.Table
206
+ ) -> ContentAST.Section:
207
+ """
208
+ Standard pattern: intro + instructions + table with blanks.
209
+
210
+ Used by: VirtualAddressParts, CachingQuestion, etc.
211
+ """
212
+ body = ContentAST.Section()
213
+ if intro_text:
214
+ body.add_element(ContentAST.Paragraph([intro_text]))
215
+ if instructions:
216
+ body.add_element(ContentAST.Paragraph([instructions]))
217
+ body.add_element(table)
218
+ return body
219
+
220
+ def create_parameter_calculation_body(
221
+ self,
222
+ intro_text: str,
223
+ parameter_table: ContentAST.Table,
224
+ answer_table: ContentAST.Table = None,
225
+ additional_instructions: str = None
226
+ ) -> ContentAST.Section:
227
+ """
228
+ Standard pattern: intro + parameter table + optional answer table.
229
+
230
+ Used by: BaseAndBounds, Paging, etc.
231
+ """
232
+ body = ContentAST.Section()
233
+ body.add_element(ContentAST.Paragraph([intro_text]))
234
+ body.add_element(parameter_table)
235
+
236
+ if additional_instructions:
237
+ body.add_element(ContentAST.Paragraph([additional_instructions]))
238
+
239
+ if answer_table:
240
+ body.add_element(answer_table)
241
+
242
+ return body
243
+
244
+
245
+ class MultiPartQuestionMixin:
246
+ """
247
+ Mixin providing multi-part question generation with labeled subparts (a), (b), (c), etc.
248
+
249
+ This mixin enables questions to be split into multiple subparts when num_subquestions > 1.
250
+ Each subpart gets its own calculation with proper (a), (b), (c) labeling and alignment.
251
+ Primarily designed for vector math questions but extensible to other question types.
252
+
253
+ Usage:
254
+ class VectorDotProduct(VectorMathQuestion, MultiPartQuestionMixin):
255
+ def get_body(self):
256
+ if self.is_multipart():
257
+ return self.create_multipart_body()
258
+ else:
259
+ return self.create_single_part_body()
260
+
261
+ Methods provided:
262
+ - is_multipart(): Check if this question should generate multiple subparts
263
+ - create_repeated_problem_part(): Create the ContentAST.RepeatedProblemPart element
264
+ - generate_subquestion_data(): Abstract method for subclasses to implement
265
+ """
266
+
267
+ def is_multipart(self):
268
+ """
269
+ Check if this question should generate multiple subparts.
270
+
271
+ Returns:
272
+ bool: True if num_subquestions > 1, False otherwise
273
+ """
274
+ return getattr(self, 'num_subquestions', 1) > 1
275
+
276
+ def create_repeated_problem_part(self, subpart_data_list):
277
+ """
278
+ Create a ContentAST.RepeatedProblemPart element from subpart data.
279
+
280
+ Args:
281
+ subpart_data_list: List of data for each subpart. Each item can be:
282
+ - A string (LaTeX equation content)
283
+ - A ContentAST.Element
284
+ - A tuple/list of elements to be joined
285
+
286
+ Returns:
287
+ ContentAST.RepeatedProblemPart: The formatted multi-part element
288
+
289
+ Example:
290
+ # For vector dot products
291
+ subparts = [
292
+ (matrix_a1, "\\cdot", matrix_b1),
293
+ (matrix_a2, "\\cdot", matrix_b2)
294
+ ]
295
+ return self.create_repeated_problem_part(subparts)
296
+ """
297
+ from QuizGenerator.contentast import ContentAST
298
+ return ContentAST.RepeatedProblemPart(subpart_data_list)
299
+
300
+ def generate_subquestion_data(self):
301
+ """
302
+ Generate data for each subpart of the question.
303
+
304
+ This is an abstract method that subclasses must implement.
305
+ It should generate and return the data needed for each subpart.
306
+
307
+ Returns:
308
+ list: List of data for each subpart. The format depends on the
309
+ specific question type but should be compatible with
310
+ ContentAST.RepeatedProblemPart.
311
+
312
+ Example implementation:
313
+ def generate_subquestion_data(self):
314
+ subparts = []
315
+ for i in range(self.num_subquestions):
316
+ vector_a = self._generate_vector(self.dimension)
317
+ vector_b = self._generate_vector(self.dimension)
318
+ matrix_a = ContentAST.Matrix.to_latex(
319
+ [[v] for v in vector_a], "b"
320
+ )
321
+ matrix_b = ContentAST.Matrix.to_latex(
322
+ [[v] for v in vector_b], "b"
323
+ )
324
+ subparts.append((matrix_a, "\\cdot", matrix_b))
325
+ return subparts
326
+ """
327
+ raise NotImplementedError(
328
+ "Subclasses using MultiPartQuestionMixin must implement generate_subquestion_data()"
329
+ )
330
+
331
+ def create_multipart_body(self, intro_text="Calculate the following:"):
332
+ """
333
+ Create a standard multipart question body using the repeated problem part format.
334
+
335
+ Args:
336
+ intro_text: Introduction text for the question
337
+
338
+ Returns:
339
+ ContentAST.Section: Complete question body with intro and subparts
340
+
341
+ Example:
342
+ def get_body(self):
343
+ if self.is_multipart():
344
+ return self.create_multipart_body("Calculate the dot products:")
345
+ else:
346
+ return self.create_single_part_body()
347
+ """
348
+ from QuizGenerator.contentast import ContentAST
349
+ body = ContentAST.Section()
350
+ body.add_element(ContentAST.Paragraph([intro_text]))
351
+
352
+ # Generate subpart data and create the repeated problem part
353
+ subpart_data = self.generate_subquestion_data()
354
+ repeated_part = self.create_repeated_problem_part(subpart_data)
355
+ body.add_element(repeated_part)
356
+
357
+ return body
358
+
359
+ def get_subpart_answers(self):
360
+ """
361
+ Retrieve answers organized by subpart for multipart questions.
362
+
363
+ Returns:
364
+ dict: Dictionary mapping subpart letters ('a', 'b', 'c') to their answers.
365
+ Returns empty dict if not a multipart question.
366
+
367
+ Example:
368
+ # For a 3-part question
369
+ {
370
+ 'a': Answer.integer('a', 5),
371
+ 'b': Answer.integer('b', 12),
372
+ 'c': Answer.integer('c', -3)
373
+ }
374
+ """
375
+ if not self.is_multipart():
376
+ return {}
377
+
378
+ subpart_answers = {}
379
+ for i in range(self.num_subquestions):
380
+ letter = chr(ord('a') + i)
381
+ # Look for answers with subpart keys
382
+ answer_key = f"subpart_{letter}"
383
+ if hasattr(self, 'answers') and answer_key in self.answers:
384
+ subpart_answers[letter] = self.answers[answer_key]
385
+
386
+ return subpart_answers
387
+
388
+
389
+ class MathOperationQuestion(MultiPartQuestionMixin, abc.ABC):
390
+ """
391
+ Abstract base class for mathematical operation questions (vectors, matrices, etc.).
392
+
393
+ This class provides common infrastructure for questions that:
394
+ - Perform operations on mathematical objects (vectors, matrices)
395
+ - Support both single and multipart questions
396
+ - Use LaTeX formatting for equations
397
+ - Generate step-by-step explanations
398
+
399
+ Subclasses must implement abstract methods for:
400
+ - Generating operands (vectors, matrices, etc.)
401
+ - Performing the mathematical operation
402
+ - Formatting results for LaTeX display
403
+ - Creating answer objects
404
+ """
405
+
406
+ def __init__(self, *args, **kwargs):
407
+ kwargs["topic"] = kwargs.get("topic", "MATH") # Default to MATH topic
408
+ super().__init__(*args, **kwargs)
409
+
410
+ # Abstract methods that subclasses must implement
411
+
412
+ @abc.abstractmethod
413
+ def get_operator(self):
414
+ """Return the LaTeX operator for this operation (e.g., '+', '\\cdot', '\\times')."""
415
+ pass
416
+
417
+ @abc.abstractmethod
418
+ def calculate_single_result(self, operand_a, operand_b):
419
+ """Calculate the result for a single question with two operands."""
420
+ pass
421
+
422
+ @abc.abstractmethod
423
+ def create_subquestion_answers(self, subpart_index, result):
424
+ """Create answer objects for a subquestion result."""
425
+ pass
426
+
427
+ @abc.abstractmethod
428
+ def generate_operands(self):
429
+ """Generate two operands for the operation. Returns (operand_a, operand_b)."""
430
+ pass
431
+
432
+ @abc.abstractmethod
433
+ def format_operand_latex(self, operand):
434
+ """Format an operand for LaTeX display."""
435
+ pass
436
+
437
+ @abc.abstractmethod
438
+ def format_single_equation(self, operand_a, operand_b):
439
+ """Format the equation for single questions. Returns LaTeX string."""
440
+ pass
441
+
442
+ # Common implementation methods
443
+
444
+ def get_intro_text(self):
445
+ """Default intro text - subclasses can override."""
446
+ return "Calculate the following:"
447
+
448
+ def create_single_answers(self, result):
449
+ """Create answers for single questions - just delegate to subquestion method."""
450
+ return self.create_subquestion_answers(0, result)
451
+
452
+ def refresh(self, *args, **kwargs):
453
+ super().refresh(*args, **kwargs)
454
+
455
+ # Clear any existing data
456
+ self.answers = {}
457
+
458
+ if self.is_multipart():
459
+ # Generate multiple subquestions
460
+ self.subquestion_data = []
461
+ for i in range(self.num_subquestions):
462
+ # Generate unique operands for each subquestion
463
+ operand_a, operand_b = self.generate_operands()
464
+ result = self.calculate_single_result(operand_a, operand_b)
465
+
466
+ self.subquestion_data.append(
467
+ {
468
+ 'operand_a': operand_a,
469
+ 'operand_b': operand_b,
470
+ 'vector_a': operand_a, # For vector compatibility
471
+ 'vector_b': operand_b, # For vector compatibility
472
+ 'result': result
473
+ }
474
+ )
475
+
476
+ # Create answers for this subpart
477
+ self.create_subquestion_answers(i, result)
478
+ else:
479
+ # Single question (original behavior)
480
+ self.operand_a, self.operand_b = self.generate_operands()
481
+ self.result = self.calculate_single_result(self.operand_a, self.operand_b)
482
+
483
+ # Create answers
484
+ self.create_single_answers(self.result)
485
+
486
+ def generate_subquestion_data(self):
487
+ """Generate LaTeX content for each subpart of the question."""
488
+ subparts = []
489
+ for data in self.subquestion_data:
490
+ operand_a_latex = self.format_operand_latex(data['operand_a'])
491
+ operand_b_latex = self.format_operand_latex(data['operand_b'])
492
+ # Return as tuple of (operand_a, operator, operand_b)
493
+ subparts.append((operand_a_latex, self.get_operator(), operand_b_latex))
494
+ return subparts
495
+
496
+ def get_body(self):
497
+ body = ContentAST.Section()
498
+
499
+ body.add_element(ContentAST.Paragraph([self.get_intro_text()]))
500
+
501
+ if self.is_multipart():
502
+ # Use multipart formatting with repeated problem parts
503
+ subpart_data = self.generate_subquestion_data()
504
+ repeated_part = self.create_repeated_problem_part(subpart_data)
505
+ body.add_element(repeated_part)
506
+ else:
507
+ # Single equation display
508
+ equation_latex = self.format_single_equation(self.operand_a, self.operand_b)
509
+ body.add_element(ContentAST.Equation(f"{equation_latex} = ", inline=False))
510
+
511
+ # Canvas-only answer fields (hidden from PDF)
512
+ self._add_single_question_answers(body)
513
+
514
+ return body
515
+
516
+ def _add_single_question_answers(self, body):
517
+ """Add Canvas-only answer fields for single questions. Subclasses can override."""
518
+ # Default implementation - subclasses should override for specific answer formats
519
+ pass
520
+
521
+ def get_explanation(self):
522
+ """Default explanation structure. Subclasses should override for specific explanations."""
523
+ explanation = ContentAST.Section()
524
+
525
+ explanation.add_element(ContentAST.Paragraph([self.get_explanation_intro()]))
526
+
527
+ if self.is_multipart():
528
+ # Handle multipart explanations
529
+ for i, data in enumerate(self.subquestion_data):
530
+ letter = chr(ord('a') + i)
531
+ explanation.add_element(self.create_explanation_for_subpart(data, letter))
532
+ else:
533
+ # Single part explanation
534
+ explanation.add_element(self.create_single_explanation())
535
+
536
+ return explanation
537
+
538
+ def get_explanation_intro(self):
539
+ """Get the intro text for explanations. Subclasses should override."""
540
+ return "The calculation is performed as follows:"
541
+
542
+ def create_explanation_for_subpart(self, subpart_data, letter):
543
+ """Create explanation for a single subpart. Subclasses should override."""
544
+ return ContentAST.Paragraph([f"Part ({letter}): Calculation details would go here."])
545
+
546
+ def create_single_explanation(self):
547
+ """Create explanation for single questions. Subclasses should override."""
548
+ return ContentAST.Paragraph(["Single question explanation would go here."])