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
QuizGenerator/misc.py ADDED
@@ -0,0 +1,480 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import decimal
5
+ import enum
6
+ import itertools
7
+ import logging
8
+ import math
9
+ from typing import List, Dict, Tuple
10
+
11
+ import fractions
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ class OutputFormat(enum.Enum):
17
+ LATEX = enum.auto(),
18
+ CANVAS = enum.auto()
19
+
20
+
21
+ class Answer:
22
+ DEFAULT_ROUNDING_DIGITS = 4
23
+
24
+ class AnswerKind(enum.Enum):
25
+ BLANK = "fill_in_multiple_blanks_question"
26
+ MULTIPLE_ANSWER = "multiple_answers_question" # todo: have baffles?
27
+ ESSAY = "essay_question"
28
+ MULTIPLE_DROPDOWN = "multiple_dropdowns_question"
29
+
30
+ class VariableKind(enum.Enum): # todo: use these for generate variations?
31
+ STR = enum.auto()
32
+ INT = enum.auto()
33
+ FLOAT = enum.auto()
34
+ BINARY = enum.auto()
35
+ HEX = enum.auto()
36
+ BINARY_OR_HEX = enum.auto()
37
+ AUTOFLOAT = enum.auto()
38
+ LIST = enum.auto()
39
+ VECTOR = enum.auto()
40
+
41
+
42
+ def __init__(
43
+ self, key:str,
44
+ value,
45
+ kind : Answer.AnswerKind = AnswerKind.BLANK,
46
+ variable_kind : Answer.VariableKind = VariableKind.STR,
47
+ display=None,
48
+ length=None,
49
+ correct=True,
50
+ baffles=None,
51
+ pdf_only=False
52
+ ):
53
+ self.key = key
54
+ self.value = value
55
+ self.kind = kind
56
+ self.variable_kind = variable_kind
57
+ # For list values in display, show the first option (or join them with /)
58
+ if display is not None:
59
+ self.display = display
60
+ elif isinstance(value, list) and variable_kind == Answer.VariableKind.STR:
61
+ self.display = value[0] if len(value) == 1 else " / ".join(value)
62
+ else:
63
+ self.display = value
64
+ self.length = length # Used for bits and hex to be printed appropriately
65
+ self.correct = correct
66
+ self.baffles = baffles
67
+ self.pdf_only = pdf_only
68
+
69
+ def get_for_canvas(self) -> List[Dict]:
70
+ # If this answer is marked as PDF-only, don't send it to Canvas
71
+ if self.pdf_only:
72
+ return []
73
+
74
+ canvas_answers : List[Dict] = []
75
+ if self.variable_kind == Answer.VariableKind.BINARY:
76
+ canvas_answers = [
77
+ {
78
+ "blank_id": self.key,
79
+ "answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
80
+ "answer_weight": 100 if self.correct else 0,
81
+ },
82
+ {
83
+ "blank_id": self.key,
84
+ "answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
85
+ "answer_weight": 100 if self.correct else 0,
86
+ }
87
+ ]
88
+ elif self.variable_kind == Answer.VariableKind.HEX:
89
+ canvas_answers = [
90
+ {
91
+ "blank_id": self.key,
92
+ "answer_text": f"{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
93
+ "answer_weight": 100 if self.correct else 0,
94
+ },{
95
+ "blank_id": self.key,
96
+ "answer_text": f"0x{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
97
+ "answer_weight": 100 if self.correct else 0,
98
+ }
99
+ ]
100
+ elif self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
101
+ canvas_answers = [
102
+ {
103
+ "blank_id": self.key,
104
+ "answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
105
+ "answer_weight": 100 if self.correct else 0,
106
+ },
107
+ {
108
+ "blank_id": self.key,
109
+ "answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
110
+ "answer_weight": 100 if self.correct else 0,
111
+ },
112
+ {
113
+ "blank_id": self.key,
114
+ "answer_text": f"{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
115
+ "answer_weight": 100 if self.correct else 0,
116
+ },
117
+ {
118
+ "blank_id": self.key,
119
+ "answer_text": f"0x{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
120
+ "answer_weight": 100 if self.correct else 0,
121
+ },
122
+ {
123
+ "blank_id": self.key,
124
+ "answer_text": f"{self.value}",
125
+ "answer_weight": 100 if self.correct else 0,
126
+ },
127
+
128
+ ]
129
+ elif self.variable_kind in [
130
+ Answer.VariableKind.AUTOFLOAT,
131
+ Answer.VariableKind.FLOAT,
132
+ Answer.VariableKind.INT
133
+ ]:
134
+ # Use the accepted_strings helper with settings that match the original AUTOFLOAT behavior
135
+ answer_strings = self.__class__.accepted_strings(
136
+ self.value,
137
+ allow_integer=True,
138
+ allow_simple_fraction=True,
139
+ max_denominator=3*4*5, # For process questions, these are the numbers of jobs we'd have
140
+ allow_mixed=True,
141
+ include_spaces=False,
142
+ include_fixed_even_if_integer=True
143
+ )
144
+
145
+ canvas_answers = [
146
+ {
147
+ "blank_id": self.key,
148
+ "answer_text": answer_string,
149
+ "answer_weight": 100 if self.correct else 0,
150
+ }
151
+ for answer_string in answer_strings
152
+ ]
153
+
154
+ elif self.variable_kind == Answer.VariableKind.VECTOR:
155
+
156
+ # Get all answer variations
157
+ answer_variations = [
158
+ self.__class__.accepted_strings(dimension_value)
159
+ for dimension_value in self.value
160
+ ]
161
+
162
+ canvas_answers = []
163
+ for combination in itertools.product(*answer_variations):
164
+ # Add parentheses format for all vectors: (1, 2, 3)
165
+ canvas_answers.append({
166
+ "blank_id" : self.key,
167
+ "answer_weight": 100 if self.correct else 0,
168
+ "answer_text": f"({', '.join(list(combination))})",
169
+ })
170
+
171
+ # Add non-parentheses format only for single-element vectors: 5
172
+ if len(combination) == 1:
173
+ canvas_answers.append(
174
+ {
175
+ "blank_id": self.key,
176
+ "answer_weight": 100 if self.correct else 0,
177
+ "answer_text": f"{', '.join(combination)}",
178
+ }
179
+ )
180
+ return canvas_answers
181
+
182
+ elif self.variable_kind == Answer.VariableKind.LIST:
183
+ canvas_answers = [
184
+ {
185
+ "blank_id": self.key,
186
+ "answer_text": ', '.join(map(str, possible_state)),
187
+ "answer_weight": 100 if self.correct else 0,
188
+ }
189
+ for possible_state in [self.value] #itertools.permutations(self.value)
190
+ ]
191
+ else:
192
+ # For string answers, check if value is a list of acceptable alternatives
193
+ if isinstance(self.value, list):
194
+ canvas_answers = [
195
+ {
196
+ "blank_id": self.key,
197
+ "answer_text": str(alt),
198
+ "answer_weight": 100 if self.correct else 0,
199
+ }
200
+ for alt in self.value
201
+ ]
202
+ else:
203
+ canvas_answers = [{
204
+ "blank_id": self.key,
205
+ "answer_text": self.value,
206
+ "answer_weight": 100 if self.correct else 0,
207
+ }]
208
+
209
+ if self.baffles is not None:
210
+ for baffle in self.baffles:
211
+ canvas_answers.append({
212
+ "blank_id": self.key,
213
+ "answer_text": baffle,
214
+ "answer_weight": 0,
215
+ })
216
+
217
+ return canvas_answers
218
+
219
+ def get_display_string(self) -> str:
220
+ """
221
+ Get the formatted display string for this answer (for grading/answer keys).
222
+
223
+ Returns the answer in the most appropriate format for human readability:
224
+ - BINARY_OR_HEX: hex format (0x...)
225
+ - BINARY: binary format (0b...)
226
+ - HEX: hex format (0x...)
227
+ - AUTOFLOAT/FLOAT: rounded to DEFAULT_ROUNDING_DIGITS
228
+ - INT: integer
229
+ - STR/LIST/VECTOR: as-is
230
+ """
231
+ if self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
232
+ # For binary_hex answers, show hex format (more compact and readable)
233
+ hex_digits = math.ceil(self.length / 4) if self.length is not None else 0
234
+ return f"0x{self.value:0{hex_digits}X}"
235
+
236
+ elif self.variable_kind == Answer.VariableKind.BINARY:
237
+ # Show binary format
238
+ return f"0b{self.value:0{self.length if self.length is not None else 0}b}"
239
+
240
+ elif self.variable_kind == Answer.VariableKind.HEX:
241
+ # Show hex format
242
+ hex_digits = (self.length // 4) + 1 if self.length is not None else 0
243
+ return f"0x{self.value:0{hex_digits}X}"
244
+
245
+ elif self.variable_kind == Answer.VariableKind.AUTOFLOAT:
246
+ # Round to default precision for readability
247
+ return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
248
+
249
+ elif self.variable_kind == Answer.VariableKind.FLOAT:
250
+ # Round to default precision
251
+ if isinstance(self.value, (list, tuple)):
252
+ return f"{round(self.value[0], self.DEFAULT_ROUNDING_DIGITS)}"
253
+ return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
254
+
255
+ elif self.variable_kind == Answer.VariableKind.INT:
256
+ return str(int(self.value))
257
+
258
+ elif self.variable_kind == Answer.VariableKind.LIST:
259
+ return ", ".join(str(v) for v in self.value)
260
+
261
+ elif self.variable_kind == Answer.VariableKind.VECTOR:
262
+ # Format as comma-separated rounded values
263
+ return ", ".join(str(round(v, self.DEFAULT_ROUNDING_DIGITS)) for v in self.value)
264
+
265
+ else:
266
+ # Default: use display or value
267
+ return str(self.display if hasattr(self, 'display') else self.value)
268
+
269
+ # Factory methods for common answer types
270
+ @classmethod
271
+ def binary_hex(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
272
+ """Create an answer that accepts binary or hex format"""
273
+ return cls(
274
+ key=key,
275
+ value=value,
276
+ variable_kind=cls.VariableKind.BINARY_OR_HEX,
277
+ length=length,
278
+ **kwargs
279
+ )
280
+
281
+ @classmethod
282
+ def auto_float(cls, key: str, value: float, **kwargs) -> 'Answer':
283
+ """Create an answer that accepts multiple float formats (decimal, fraction, mixed)"""
284
+ return cls(
285
+ key=key,
286
+ value=value,
287
+ variable_kind=cls.VariableKind.AUTOFLOAT,
288
+ **kwargs
289
+ )
290
+
291
+ @classmethod
292
+ def integer(cls, key: str, value: int, **kwargs) -> 'Answer':
293
+ """Create an integer answer"""
294
+ return cls(
295
+ key=key,
296
+ value=value,
297
+ variable_kind=cls.VariableKind.INT,
298
+ **kwargs
299
+ )
300
+
301
+ @classmethod
302
+ def string(cls, key: str, value: str, **kwargs) -> 'Answer':
303
+ """Create a string answer"""
304
+ return cls(
305
+ key=key,
306
+ value=value,
307
+ variable_kind=cls.VariableKind.STR,
308
+ **kwargs
309
+ )
310
+
311
+ @classmethod
312
+ def binary(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
313
+ """Create a binary-only answer"""
314
+ return cls(
315
+ key=key,
316
+ value=value,
317
+ variable_kind=cls.VariableKind.BINARY,
318
+ length=length,
319
+ **kwargs
320
+ )
321
+
322
+ @classmethod
323
+ def hex_value(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
324
+ """Create a hex-only answer"""
325
+ return cls(
326
+ key=key,
327
+ value=value,
328
+ variable_kind=cls.VariableKind.HEX,
329
+ length=length,
330
+ **kwargs
331
+ )
332
+
333
+ @classmethod
334
+ def float_value(cls, key: str, value: Tuple[float], **kwargs) -> 'Answer':
335
+ """Create a simple float answer (no fraction conversion)"""
336
+ return cls(
337
+ key=key,
338
+ value=value,
339
+ variable_kind=cls.VariableKind.FLOAT,
340
+ **kwargs
341
+ )
342
+
343
+ @classmethod
344
+ def list_value(cls, key: str, value: list, **kwargs) -> 'Answer':
345
+ """Create a list answer (comma-separated values)"""
346
+ return cls(
347
+ key=key,
348
+ value=value,
349
+ variable_kind=cls.VariableKind.LIST,
350
+ **kwargs
351
+ )
352
+
353
+ @classmethod
354
+ def vector_value(cls, key: str, value: List[float], **kwargs) -> 'Answer':
355
+ """Create a simple float answer (no fraction conversion)"""
356
+ return cls(
357
+ key=key,
358
+ value=value,
359
+ variable_kind=cls.VariableKind.VECTOR,
360
+ **kwargs
361
+ )
362
+
363
+ @classmethod
364
+ def dropdown(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
365
+ """Create a dropdown answer with wrong answer choices (baffles)"""
366
+ return cls(
367
+ key=key,
368
+ value=value,
369
+ kind=cls.AnswerKind.MULTIPLE_DROPDOWN,
370
+ baffles=baffles,
371
+ **kwargs
372
+ )
373
+
374
+ @classmethod
375
+ def multiple_choice(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
376
+ """Create a multiple choice answer with wrong answer choices (baffles)"""
377
+ return cls(
378
+ key=key,
379
+ value=value,
380
+ kind=cls.AnswerKind.MULTIPLE_ANSWER,
381
+ baffles=baffles,
382
+ **kwargs
383
+ )
384
+
385
+ @classmethod
386
+ def essay(cls, key: str, **kwargs) -> 'Answer':
387
+ """Create an essay question (no specific correct answer)"""
388
+ return cls(
389
+ key=key,
390
+ value="", # Essays don't have predetermined answers
391
+ kind=cls.AnswerKind.ESSAY,
392
+ **kwargs
393
+ )
394
+
395
+ @staticmethod
396
+ def _to_fraction(x):
397
+ """Convert int/float/decimal.Decimal/fractions.Fraction/str('a/b' or decimal) to fractions.Fraction exactly."""
398
+ if isinstance(x, fractions.Fraction):
399
+ return x
400
+ if isinstance(x, int):
401
+ return fractions.Fraction(x, 1)
402
+ if isinstance(x, decimal.Decimal):
403
+ # exact conversion of decimal.Decimal to fractions.Fraction
404
+ sign, digits, exp = x.as_tuple()
405
+ n = 0
406
+ for d in digits:
407
+ n = n * 10 + d
408
+ n = -n if sign else n
409
+ if exp >= 0:
410
+ return fractions.Fraction(n * (10 ** exp), 1)
411
+ else:
412
+ return fractions.Fraction(n, 10 ** (-exp))
413
+ if isinstance(x, str):
414
+ s = x.strip()
415
+ if '/' in s:
416
+ a, b = s.split('/', 1)
417
+ return fractions.Fraction(int(a.strip()), int(b.strip()))
418
+ return fractions.Fraction(decimal.Decimal(s))
419
+ # float or other numerics
420
+ return fractions.Fraction(decimal.Decimal(str(x)))
421
+
422
+ @staticmethod
423
+ def accepted_strings(
424
+ value,
425
+ *,
426
+ allow_integer=True, # allow "whole numbers as whole numbers"
427
+ allow_simple_fraction=True, # allow simple a/b when denominator small
428
+ max_denominator=720, # how "simple" the fraction is
429
+ allow_mixed=False, # also allow "1 1/2" for 3/2
430
+ include_spaces=False, # also accept "1 / 2"
431
+ include_fixed_even_if_integer=False # include "1.0000" when value is 1 and fixed_decimals is set
432
+ ):
433
+ """
434
+ Return a sorted list of strings you can paste into Canvas as alternate correct answers.
435
+ """
436
+ decimal.getcontext().prec = max(34, (Answer.DEFAULT_ROUNDING_DIGITS or 0) + 10)
437
+ f = Answer._to_fraction(value)
438
+ outs = set()
439
+
440
+ # Integer form
441
+ if f.denominator == 1 and allow_integer:
442
+ outs.add(str(f.numerator))
443
+ if include_fixed_even_if_integer:
444
+ q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS) # 1e-<fixed_decimals>
445
+ d = decimal.Decimal(f.numerator).quantize(q, rounding=decimal.ROUND_HALF_UP)
446
+ outs.add(format(d, 'f'))
447
+
448
+ # Fixed-decimal form (exactly N decimals)
449
+ q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
450
+ d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
451
+ outs.add(format(d, 'f'))
452
+
453
+ # Trimmed decimal (no trailing zeros; up to max_trimmed_decimals)
454
+ if Answer.DEFAULT_ROUNDING_DIGITS:
455
+ q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
456
+ d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
457
+ s = format(d, 'f').rstrip('0').rstrip('.')
458
+ # ensure we keep leading zero like "0.5"
459
+ if s.startswith('.'):
460
+ s = '0' + s
461
+ if s == '-0': # tidy negative zero
462
+ s = '0'
463
+ outs.add(s)
464
+
465
+ # Simple fraction (reduced, with small denominator)
466
+ if allow_simple_fraction:
467
+ fr = f.limit_denominator(max_denominator)
468
+ if fr == f:
469
+ a, b = fr.numerator, fr.denominator
470
+ outs.add(f"{a}/{b}")
471
+ if include_spaces:
472
+ outs.add(f"{a} / {b}")
473
+ if allow_mixed and b != 1 and abs(a) > b:
474
+ sign = '-' if a < 0 else ''
475
+ A = abs(a)
476
+ whole, rem = divmod(A, b)
477
+ outs.add(f"{sign}{whole} {rem}/{b}")
478
+
479
+ return sorted(outs, key=lambda s: (len(s), s))
480
+