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,293 @@
1
+ """
2
+ QR Code generation module for quiz questions.
3
+
4
+ This module generates QR codes containing question metadata (question number,
5
+ points value, etc.) that can be embedded in PDF output for scanning and
6
+ automated grading.
7
+
8
+ The QR codes include encrypted data that allows regenerating question answers
9
+ without storing separate files, enabling efficient grading of randomized exams.
10
+ """
11
+
12
+ import json
13
+ import tempfile
14
+ import logging
15
+ import os
16
+ import base64
17
+ from io import BytesIO
18
+ from pathlib import Path
19
+ from typing import Optional, Dict, Any
20
+
21
+ import segno
22
+ from cryptography.fernet import Fernet
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ class QuestionQRCode:
28
+ """
29
+ Generator for question metadata QR codes.
30
+
31
+ QR codes encode question information in JSON format for easy parsing
32
+ after scanning. They use high error correction (30% recovery) to ensure
33
+ reliability when printed and scanned.
34
+ """
35
+
36
+ # QR code size in cm for LaTeX output (suitable for 200 DPI scanning)
37
+ DEFAULT_SIZE_CM = 1.5 # Compact size suitable for ~60 char encoded data
38
+
39
+ # Error correction level: M = 15% recovery (balanced for compact encoded data)
40
+ ERROR_CORRECTION = 'M'
41
+
42
+ @classmethod
43
+ def get_encryption_key(cls) -> bytes:
44
+ """
45
+ Get encryption key from environment or generate new one.
46
+
47
+ The key is loaded from QUIZ_ENCRYPTION_KEY environment variable.
48
+ If not set, generates a new key (for development only).
49
+
50
+ Returns:
51
+ bytes: Fernet encryption key
52
+
53
+ Note:
54
+ In production, always set QUIZ_ENCRYPTION_KEY environment variable!
55
+ Generate a key once with: Fernet.generate_key()
56
+ """
57
+ key_str = os.environ.get('QUIZ_ENCRYPTION_KEY')
58
+
59
+ if key_str is None:
60
+ log.warning(
61
+ "QUIZ_ENCRYPTION_KEY not set! Generating temporary key. "
62
+ "Set this environment variable for production use!"
63
+ )
64
+ # Generate temporary key for development
65
+ return Fernet.generate_key()
66
+
67
+ # Key should be stored as base64 string in env
68
+ return key_str.encode()
69
+
70
+ @classmethod
71
+ def encrypt_question_data(cls, question_type: str, seed: int, version: str,
72
+ config: Optional[Dict[str, Any]] = None,
73
+ key: Optional[bytes] = None) -> str:
74
+ """
75
+ Encode question regeneration data with optional simple obfuscation.
76
+
77
+ Args:
78
+ question_type: Class name of the question (e.g., "VectorDotProduct")
79
+ seed: Random seed used to generate this specific question
80
+ version: Question class version (e.g., "1.0")
81
+ config: Optional dictionary of configuration parameters
82
+ key: Encryption key (uses environment key if None)
83
+
84
+ Returns:
85
+ str: Base64-encoded (optionally XOR-obfuscated) data
86
+
87
+ Example:
88
+ >>> encrypted = QuestionQRCode.encrypt_question_data("VectorDot", 12345, "1.0")
89
+ >>> print(encrypted)
90
+ 'VmVjdG9yRG90OjEyMzQ1OjEuMA=='
91
+ """
92
+ # Create compact data string, including config if provided
93
+ if config:
94
+ # Serialize config as JSON and append to data string
95
+ config_json = json.dumps(config, separators=(',', ':'))
96
+ data_str = f"{question_type}:{seed}:{version}:{config_json}"
97
+ else:
98
+ data_str = f"{question_type}:{seed}:{version}"
99
+ data_bytes = data_str.encode('utf-8')
100
+
101
+ # Simple XOR obfuscation if key is provided (optional, for basic protection)
102
+ if key is None:
103
+ key = cls.get_encryption_key()
104
+
105
+ if key:
106
+ # Use first 16 bytes of key for simple XOR obfuscation
107
+ key_bytes = key[:16] if isinstance(key, bytes) else key.encode()[:16]
108
+ # XOR each byte with repeating key pattern
109
+ obfuscated = bytes(b ^ key_bytes[i % len(key_bytes)] for i, b in enumerate(data_bytes))
110
+ else:
111
+ obfuscated = data_bytes
112
+
113
+ # Base64 encode for compact representation
114
+ encoded = base64.urlsafe_b64encode(obfuscated).decode('utf-8')
115
+
116
+ log.debug(f"Encoded question data: {question_type} seed={seed} version={version} ({len(encoded)} chars)")
117
+
118
+ return encoded
119
+
120
+ @classmethod
121
+ def decrypt_question_data(cls, encrypted_data: str,
122
+ key: Optional[bytes] = None) -> Dict[str, Any]:
123
+ """
124
+ Decode question regeneration data from QR code.
125
+
126
+ Args:
127
+ encrypted_data: Base64-encoded (optionally XOR-obfuscated) string from QR code
128
+ key: Encryption key (uses environment key if None)
129
+
130
+ Returns:
131
+ dict: {"question_type": str, "seed": int, "version": str, "config": dict (optional)}
132
+
133
+ Raises:
134
+ ValueError: If decoding fails or data is malformed
135
+
136
+ Example:
137
+ >>> data = QuestionQRCode.decrypt_question_data("VmVjdG9yRG90OjEyMzQ1OjEuMA==")
138
+ >>> print(data)
139
+ {"question_type": "VectorDot", "seed": 12345, "version": "1.0"}
140
+ """
141
+ if key is None:
142
+ key = cls.get_encryption_key()
143
+
144
+ try:
145
+ # Decode from base64
146
+ obfuscated = base64.urlsafe_b64decode(encrypted_data.encode())
147
+
148
+ # Reverse XOR obfuscation if key is provided
149
+ if key:
150
+ key_bytes = key[:16] if isinstance(key, bytes) else key.encode()[:16]
151
+ data_bytes = bytes(b ^ key_bytes[i % len(key_bytes)] for i, b in enumerate(obfuscated))
152
+ else:
153
+ data_bytes = obfuscated
154
+
155
+ data_str = data_bytes.decode('utf-8')
156
+
157
+ # Parse data string - can be 3 or 4 parts (4th is optional config)
158
+ parts = data_str.split(':', 3) # Split into max 4 parts
159
+ if len(parts) < 3:
160
+ raise ValueError(f"Invalid encoded data format: expected at least 3 parts, got {len(parts)}")
161
+
162
+ question_type = parts[0]
163
+ seed_str = parts[1]
164
+ version = parts[2]
165
+
166
+ result = {
167
+ "question_type": question_type,
168
+ "seed": int(seed_str),
169
+ "version": version
170
+ }
171
+
172
+ # Parse config JSON if present
173
+ if len(parts) == 4:
174
+ try:
175
+ result["config"] = json.loads(parts[3])
176
+ except json.JSONDecodeError as e:
177
+ log.warning(f"Failed to parse config JSON: {e}")
178
+ # Continue without config rather than failing
179
+
180
+ return result
181
+
182
+ except Exception as e:
183
+ log.error(f"Failed to decode question data: {e}")
184
+ raise ValueError(f"Failed to decode QR code data: {e}")
185
+
186
+ @classmethod
187
+ def generate_qr_data(cls, question_number: int, points_value: float, **extra_data) -> str:
188
+ """
189
+ Generate JSON string containing question metadata.
190
+
191
+ Args:
192
+ question_number: Sequential question number in the quiz
193
+ points_value: Point value of the question
194
+ **extra_data: Additional metadata to include
195
+ - question_type (str): Question class name for regeneration
196
+ - seed (int): Random seed used for this question
197
+ - version (str): Question class version
198
+ - config (dict): Question-specific configuration parameters
199
+
200
+ Returns:
201
+ JSON string with question metadata
202
+
203
+ Example:
204
+ >>> QuestionQRCode.generate_qr_data(1, 5.0)
205
+ '{"q": 1, "pts": 5.0}'
206
+
207
+ >>> QuestionQRCode.generate_qr_data(
208
+ ... 2, 10,
209
+ ... question_type="VectorDot",
210
+ ... seed=12345,
211
+ ... version="1.0",
212
+ ... config={"max_value": 100}
213
+ ... )
214
+ '{"q": 2, "pts": 10, "s": "gAAAAAB..."}'
215
+ """
216
+ data = {
217
+ "q": question_number,
218
+ "pts": points_value
219
+ }
220
+
221
+ # If question regeneration data provided, encrypt it
222
+ if all(k in extra_data for k in ['question_type', 'seed', 'version']):
223
+ # Include config in encrypted data if present
224
+ config = extra_data.get('config', {})
225
+ encrypted = cls.encrypt_question_data(
226
+ extra_data['question_type'],
227
+ extra_data['seed'],
228
+ extra_data['version'],
229
+ config=config
230
+ )
231
+ data['s'] = encrypted
232
+
233
+ # Remove the unencrypted data
234
+ extra_data = {k: v for k, v in extra_data.items()
235
+ if k not in ['question_type', 'seed', 'version', 'config']}
236
+
237
+ # Add any remaining extra metadata
238
+ data.update(extra_data)
239
+
240
+ return json.dumps(data, separators=(',', ':'))
241
+
242
+ @classmethod
243
+ def generate_qr_pdf(cls, question_number: int, points_value: float,
244
+ scale: int = 10, **extra_data) -> str:
245
+ """
246
+ Generate QR code and save as PNG file, returning the file path.
247
+
248
+ This is used for LaTeX inclusion via \\includegraphics.
249
+ The file is saved to a temporary location that LaTeX can access.
250
+
251
+ Args:
252
+ question_number: Sequential question number
253
+ points_value: Point value of the question
254
+ scale: Scale factor for PNG generation (higher = larger file, better quality)
255
+ **extra_data: Additional metadata
256
+
257
+ Returns:
258
+ Path to generated PNG file
259
+ """
260
+ qr_data = cls.generate_qr_data(question_number, points_value, **extra_data)
261
+
262
+ # Generate QR code with high error correction
263
+ qr = segno.make(qr_data, error=cls.ERROR_CORRECTION)
264
+
265
+ # Create temporary file for the PNG
266
+ # We use a predictable name based on question number so LaTeX can find it
267
+ temp_dir = Path(tempfile.gettempdir()) / "quiz_qrcodes"
268
+ temp_dir.mkdir(exist_ok=True)
269
+
270
+ qr_path = temp_dir / f"qr_q{question_number}.pdf"
271
+
272
+ # Save as PNG with appropriate scale
273
+ qr.save(str(qr_path), scale=scale, border=0)
274
+
275
+ log.debug(f"Generated QR code for question {question_number} at {qr_path}")
276
+
277
+ return str(qr_path)
278
+
279
+ @classmethod
280
+ def cleanup_temp_files(cls):
281
+ """
282
+ Clean up temporary QR code files.
283
+
284
+ Call this after PDF generation is complete to remove temporary files.
285
+ """
286
+ temp_dir = Path(tempfile.gettempdir()) / "quiz_qrcodes"
287
+ if temp_dir.exists():
288
+ for qr_file in temp_dir.glob("qr_q*.png"):
289
+ try:
290
+ qr_file.unlink()
291
+ log.debug(f"Cleaned up QR code file: {qr_file}")
292
+ except Exception as e:
293
+ log.warning(f"Failed to clean up {qr_file}: {e}")