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