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,235 @@
1
+ #!env python
2
+ from __future__ import annotations
3
+
4
+ import enum
5
+ import logging
6
+ import dataclasses
7
+ import functools
8
+ import io
9
+ import os
10
+ import urllib.request
11
+ from typing import Optional, List, Dict
12
+
13
+ import canvasapi.canvas
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+
19
+ class LMSWrapper():
20
+ def __init__(self, _inner):
21
+ self._inner = _inner
22
+
23
+ def __getattr__(self, name):
24
+ try:
25
+ # Try to get the attribute from the inner instance
26
+ return getattr(self._inner, name)
27
+ except AttributeError:
28
+ # Handle the case where the inner instance also doesn't have the attribute
29
+ print(f"Warning: '{name}' not found in either wrapper or inner class")
30
+ # You can raise the error again, return None, or handle it however you want
31
+ return lambda *args, **kwargs: None # Returns a no-op function for method calls
32
+
33
+
34
+ @dataclasses.dataclass
35
+ class Student(LMSWrapper):
36
+ name : str
37
+ user_id : int
38
+ _inner : canvasapi.canvas.User
39
+
40
+
41
+ class Submission:
42
+
43
+ class Status(enum.Enum):
44
+ MISSING = "unsubmitted"
45
+ UNGRADED = ("submitted", "pending_review")
46
+ GRADED = "graded"
47
+
48
+ @classmethod
49
+ def from_string(cls, status_string, current_score):
50
+ for status in cls:
51
+ if status is not cls.MISSING and current_score is None:
52
+ return cls.UNGRADED
53
+ if isinstance(status.value, tuple):
54
+ if status_string in status.value:
55
+ return status
56
+ elif status_string == status.value:
57
+ return status
58
+ return cls.MISSING # Default
59
+
60
+
61
+ def __init__(
62
+ self,
63
+ *,
64
+ student : Student = None,
65
+ status : Submission.Status = Status.UNGRADED,
66
+ **kwargs
67
+ ):
68
+ self._student: Optional[Student] = student
69
+ self.status = status
70
+ self.input_files = None
71
+ self.feedback : Optional[Feedback] = None
72
+ self.extra_info = {}
73
+
74
+ @property
75
+ def student(self):
76
+ return self._student
77
+
78
+ @student.setter
79
+ def student(self, student):
80
+ self._student = student
81
+
82
+ def __str__(self):
83
+ try:
84
+ return f"Submission({self.student.name} : {self.feedback})"
85
+ except AttributeError:
86
+ return f"Submission({self.student} : {self.feedback})"
87
+
88
+ def set_extra(self, extras_dict: Dict):
89
+ self.extra_info.update(extras_dict)
90
+
91
+
92
+ class FileSubmission(Submission):
93
+ """Base class for submissions that contain files (e.g., programming assignments)"""
94
+ def __init__(self, *args, **kwargs):
95
+ super().__init__(*args, **kwargs)
96
+ self._files = None
97
+
98
+ @property
99
+ def files(self):
100
+ return self._files
101
+
102
+ @files.setter
103
+ def files(self, files):
104
+ self._files = files
105
+
106
+
107
+ class FileSubmission__Canvas(FileSubmission):
108
+ """Canvas-specific file submission with attachment downloading"""
109
+ def __init__(self, *args, attachments : Optional[List], **kwargs):
110
+ super().__init__(*args, **kwargs)
111
+ self._attachments = attachments
112
+ self.submission_index = kwargs.get("submission_index", None)
113
+
114
+ @property
115
+ def files(self):
116
+ # Check if we have already downloaded the files locally and return if we have
117
+ if self._files is not None:
118
+ return self._files
119
+
120
+ # If we haven't downloaded the files yet, check if we have attachments and can download them
121
+ if self._attachments is not None:
122
+ self._files = []
123
+ for attachment in self._attachments:
124
+
125
+ # Generate a local file name with a number of options
126
+ # local_file_name = f"{self.student.name.replace(' ', '-')}_{self.student.user_id}_{attachment['filename']}"
127
+ local_file_name = f"{attachment['filename']}"
128
+ with urllib.request.urlopen(attachment['url']) as response:
129
+ buffer = io.BytesIO(response.read())
130
+ buffer.name = local_file_name
131
+ self._files.append(buffer)
132
+
133
+ return self._files
134
+
135
+
136
+ class TextSubmission(Submission):
137
+ """Submission containing text content (e.g., journal entries, essays)"""
138
+ def __init__(self, *args, submission_text=None, **kwargs):
139
+ super().__init__(*args, **kwargs)
140
+ self.submission_text = submission_text or ""
141
+
142
+ def get_text(self):
143
+ """Get the submission text content"""
144
+ return self.submission_text
145
+
146
+ def get_word_count(self):
147
+ """Get word count of the submission"""
148
+ return len(self.submission_text.split()) if self.submission_text else 0
149
+
150
+ def get_character_count(self, include_spaces=True):
151
+ """Get character count of the submission"""
152
+ if not self.submission_text:
153
+ return 0
154
+ return len(self.submission_text) if include_spaces else len(self.submission_text.replace(' ', ''))
155
+
156
+ def get_paragraph_count(self):
157
+ """Get paragraph count (separated by double newlines)"""
158
+ if not self.submission_text:
159
+ return 0
160
+ paragraphs = [p.strip() for p in self.submission_text.split('\n\n') if p.strip()]
161
+ return len(paragraphs)
162
+
163
+ def __str__(self):
164
+ try:
165
+ word_count = self.get_word_count()
166
+ return f"TextSubmission({self.student.name} : {word_count} words : {self.feedback})"
167
+ except AttributeError:
168
+ return f"TextSubmission({self.student} : {self.get_word_count()} words : {self.feedback})"
169
+
170
+
171
+ class TextSubmission__Canvas(TextSubmission):
172
+ """Canvas-specific text submission"""
173
+ def __init__(self, *args, canvas_submission_data=None, **kwargs):
174
+ submission_text = ""
175
+ if canvas_submission_data and hasattr(canvas_submission_data, 'body') and canvas_submission_data.body:
176
+ submission_text = canvas_submission_data.body
177
+
178
+ super().__init__(*args, submission_text=submission_text, **kwargs)
179
+ self.canvas_submission_data = canvas_submission_data
180
+ self.submission_index = kwargs.get("submission_index", None)
181
+
182
+
183
+ class QuizSubmission(Submission):
184
+ """Submission containing quiz responses and question metadata"""
185
+ def __init__(self, *args, quiz_submission_data=None, student_responses=None, quiz_questions=None, **kwargs):
186
+ super().__init__(*args, **kwargs)
187
+ self.quiz_submission_data = quiz_submission_data
188
+ self.responses = student_responses or {} # Dict mapping question_id -> response
189
+ self.questions = quiz_questions or {} # Dict mapping question_id -> question metadata
190
+
191
+ def get_response(self, question_id: int):
192
+ """Get student's response to a specific question"""
193
+ return self.responses.get(question_id)
194
+
195
+ def get_question(self, question_id: int):
196
+ """Get question metadata for a specific question"""
197
+ return self.questions.get(question_id)
198
+
199
+ def __str__(self):
200
+ try:
201
+ response_count = len(self.responses)
202
+ return f"QuizSubmission({self.student.name} : {response_count} responses : {self.feedback})"
203
+ except AttributeError:
204
+ return f"QuizSubmission({self.student} : {len(self.responses)} responses : {self.feedback})"
205
+
206
+
207
+ # Maintain backward compatibility
208
+ Submission__Canvas = FileSubmission__Canvas
209
+
210
+
211
+ @functools.total_ordering
212
+ @dataclasses.dataclass
213
+ class Feedback:
214
+ percentage_score: Optional[float] = None
215
+ comments: str = ""
216
+ attachments: List[io.BytesIO] = dataclasses.field(default_factory=list)
217
+
218
+ def __str__(self):
219
+ short_comment = self.comments[:10].replace('\n', '\\n')
220
+ ellipsis = '...' if len(self.comments) > 10 else ''
221
+ return f"Feedback({self.percentage_score:.4g}%, {short_comment}{ellipsis})"
222
+
223
+ def __eq__(self, other):
224
+ if not isinstance(other, Feedback):
225
+ return NotImplemented
226
+ return self.percentage_score == other.percentage_score
227
+
228
+ def __lt__(self, other):
229
+ if not isinstance(other, Feedback):
230
+ return NotImplemented
231
+ if self.percentage_score is None:
232
+ return False # None is treated as greater than any other value
233
+ if other.percentage_score is None:
234
+ return True
235
+ return self.percentage_score < other.percentage_score
@@ -0,0 +1,149 @@
1
+ #!env python
2
+ """
3
+ Common constants used across question types.
4
+ Centralizing these values makes it easier to maintain consistency
5
+ and adjust ranges globally.
6
+ """
7
+
8
+ # Bit-related constants
9
+ class BitRanges:
10
+ DEFAULT_MIN_BITS = 3
11
+ DEFAULT_MAX_BITS = 16
12
+
13
+ # Memory addressing specific (using standardized names)
14
+ DEFAULT_MIN_BITS_VA = 5
15
+ DEFAULT_MAX_BITS_VA = 10
16
+ DEFAULT_MIN_BITS_OFFSET = 3
17
+ DEFAULT_MAX_BITS_OFFSET = 8
18
+ DEFAULT_MIN_BITS_VPN = 3
19
+ DEFAULT_MAX_BITS_VPN = 8
20
+ DEFAULT_MIN_BITS_PFN = 3
21
+ DEFAULT_MAX_BITS_PFN = 16
22
+
23
+ # Backward compatibility - deprecated, use standardized names above
24
+ DEFAULT_MIN_VA_BITS = DEFAULT_MIN_BITS_VA
25
+ DEFAULT_MAX_VA_BITS = DEFAULT_MAX_BITS_VA
26
+ DEFAULT_MIN_OFFSET_BITS = DEFAULT_MIN_BITS_OFFSET
27
+ DEFAULT_MAX_OFFSET_BITS = DEFAULT_MAX_BITS_OFFSET
28
+ DEFAULT_MIN_VPN_BITS = DEFAULT_MIN_BITS_VPN
29
+ DEFAULT_MAX_VPN_BITS = DEFAULT_MAX_BITS_VPN
30
+ DEFAULT_MIN_PFN_BITS = DEFAULT_MIN_BITS_PFN
31
+ DEFAULT_MAX_PFN_BITS = DEFAULT_MAX_BITS_PFN
32
+
33
+ # Base and bounds
34
+ DEFAULT_MAX_ADDRESS_BITS = 32
35
+ DEFAULT_MIN_BOUNDS_BITS = 5
36
+ DEFAULT_MAX_BOUNDS_BITS = 16
37
+
38
+ # Job/Process constants
39
+ class ProcessRanges:
40
+ DEFAULT_MIN_JOBS = 2
41
+ DEFAULT_MAX_JOBS = 5
42
+ DEFAULT_MIN_DURATION = 2
43
+ DEFAULT_MAX_DURATION = 10
44
+ DEFAULT_MIN_ARRIVAL_TIME = 0
45
+ DEFAULT_MAX_ARRIVAL_TIME = 20
46
+
47
+ # Cache/Memory constants
48
+ class CacheRanges:
49
+ DEFAULT_MIN_CACHE_SIZE = 2
50
+ DEFAULT_MAX_CACHE_SIZE = 8
51
+ DEFAULT_MIN_ELEMENTS = 3
52
+ DEFAULT_MAX_ELEMENTS = 10
53
+ DEFAULT_MIN_REQUESTS = 5
54
+ DEFAULT_MAX_REQUESTS = 20
55
+
56
+ # Disk/IO constants
57
+ class IOConstants:
58
+ DEFAULT_MIN_RPM = 3600
59
+ DEFAULT_MAX_RPM = 15000
60
+ DEFAULT_MIN_SEEK_DELAY = 3.0
61
+ DEFAULT_MAX_SEEK_DELAY = 20.0
62
+ DEFAULT_MIN_TRANSFER_RATE = 50
63
+ DEFAULT_MAX_TRANSFER_RATE = 300
64
+
65
+ # Math question constants
66
+ class MathRanges:
67
+ DEFAULT_MIN_MATH_BITS = 3
68
+ DEFAULT_MAX_MATH_BITS = 49
69
+
70
+
71
+ # =============================================================================
72
+ # Utility Functions
73
+ # =============================================================================
74
+
75
+ def get_bit_range(bit_type: str) -> tuple[int, int]:
76
+ """
77
+ Get the min/max range for a specific type of bit parameter.
78
+
79
+ Args:
80
+ bit_type: One of 'va', 'offset', 'vpn', 'pfn', 'general'
81
+
82
+ Returns:
83
+ Tuple of (min_bits, max_bits)
84
+
85
+ Raises:
86
+ ValueError: If bit_type is not recognized
87
+ """
88
+ ranges = {
89
+ 'va': (BitRanges.DEFAULT_MIN_BITS_VA, BitRanges.DEFAULT_MAX_BITS_VA),
90
+ 'offset': (BitRanges.DEFAULT_MIN_BITS_OFFSET, BitRanges.DEFAULT_MAX_BITS_OFFSET),
91
+ 'vpn': (BitRanges.DEFAULT_MIN_BITS_VPN, BitRanges.DEFAULT_MAX_BITS_VPN),
92
+ 'pfn': (BitRanges.DEFAULT_MIN_BITS_PFN, BitRanges.DEFAULT_MAX_BITS_PFN),
93
+ 'general': (BitRanges.DEFAULT_MIN_BITS, BitRanges.DEFAULT_MAX_BITS),
94
+ }
95
+
96
+ if bit_type not in ranges:
97
+ raise ValueError(f"Unknown bit_type '{bit_type}'. Valid types: {list(ranges.keys())}")
98
+
99
+ return ranges[bit_type]
100
+
101
+
102
+ def get_process_range(param_type: str) -> tuple[int, int]:
103
+ """
104
+ Get the min/max range for job/process parameters.
105
+
106
+ Args:
107
+ param_type: One of 'jobs', 'duration', 'arrival_time'
108
+
109
+ Returns:
110
+ Tuple of (min_value, max_value)
111
+
112
+ Raises:
113
+ ValueError: If param_type is not recognized
114
+ """
115
+ ranges = {
116
+ 'jobs': (ProcessRanges.DEFAULT_MIN_JOBS, ProcessRanges.DEFAULT_MAX_JOBS),
117
+ 'duration': (ProcessRanges.DEFAULT_MIN_DURATION, ProcessRanges.DEFAULT_MAX_DURATION),
118
+ 'arrival_time': (ProcessRanges.DEFAULT_MIN_ARRIVAL_TIME, ProcessRanges.DEFAULT_MAX_ARRIVAL_TIME),
119
+ }
120
+
121
+ if param_type not in ranges:
122
+ raise ValueError(f"Unknown param_type '{param_type}'. Valid types: {list(ranges.keys())}")
123
+
124
+ return ranges[param_type]
125
+
126
+
127
+ def get_cache_range(param_type: str) -> tuple[int, int]:
128
+ """
129
+ Get the min/max range for cache/memory parameters.
130
+
131
+ Args:
132
+ param_type: One of 'cache_size', 'elements', 'requests'
133
+
134
+ Returns:
135
+ Tuple of (min_value, max_value)
136
+
137
+ Raises:
138
+ ValueError: If param_type is not recognized
139
+ """
140
+ ranges = {
141
+ 'cache_size': (CacheRanges.DEFAULT_MIN_CACHE_SIZE, CacheRanges.DEFAULT_MAX_CACHE_SIZE),
142
+ 'elements': (CacheRanges.DEFAULT_MIN_ELEMENTS, CacheRanges.DEFAULT_MAX_ELEMENTS),
143
+ 'requests': (CacheRanges.DEFAULT_MIN_REQUESTS, CacheRanges.DEFAULT_MAX_REQUESTS),
144
+ }
145
+
146
+ if param_type not in ranges:
147
+ raise ValueError(f"Unknown param_type '{param_type}'. Valid types: {list(ranges.keys())}")
148
+
149
+ return ranges[param_type]