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.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +622 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1809 -0
- QuizGenerator/generate.py +362 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +480 -0
- QuizGenerator/mixins.py +539 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +395 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1398 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +396 -0
- QuizGenerator/premade_questions/cst334/process.py +649 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1264 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +657 -0
- QuizGenerator/quiz.py +468 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.1.0.dist-info/METADATA +263 -0
- quizgenerator-0.1.0.dist-info/RECORD +44 -0
- quizgenerator-0.1.0.dist-info/WHEEL +4 -0
- quizgenerator-0.1.0.dist-info/entry_points.txt +2 -0
- quizgenerator-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Performance instrumentation for quiz generation and Canvas upload pipeline.
|
|
3
|
+
|
|
4
|
+
This module provides timing instrumentation to identify bottlenecks in:
|
|
5
|
+
1. Question generation (refresh, get_body, get_explanation)
|
|
6
|
+
2. AST rendering for Canvas
|
|
7
|
+
3. Canvas API uploads
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from QuizGenerator.performance import PerformanceTracker, timer
|
|
11
|
+
|
|
12
|
+
# Use context manager
|
|
13
|
+
with timer("operation_name"):
|
|
14
|
+
# your code here
|
|
15
|
+
|
|
16
|
+
# Access metrics
|
|
17
|
+
metrics = PerformanceTracker.get_metrics()
|
|
18
|
+
PerformanceTracker.report_summary()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
import statistics
|
|
23
|
+
import logging
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from typing import Dict, List, Optional, Any
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TimingMetric:
|
|
33
|
+
"""Container for timing measurements"""
|
|
34
|
+
operation: str
|
|
35
|
+
duration: float
|
|
36
|
+
question_name: Optional[str] = None
|
|
37
|
+
question_type: Optional[str] = None
|
|
38
|
+
variation_number: Optional[int] = None
|
|
39
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
class PerformanceTracker:
|
|
42
|
+
"""Global performance tracking system"""
|
|
43
|
+
|
|
44
|
+
_metrics: List[TimingMetric] = []
|
|
45
|
+
_active_timers: Dict[str, float] = {}
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def start_timer(cls, operation: str, **metadata) -> str:
|
|
49
|
+
"""Start a named timer and return a timer ID"""
|
|
50
|
+
timer_id = f"{operation}_{time.time()}"
|
|
51
|
+
cls._active_timers[timer_id] = time.perf_counter()
|
|
52
|
+
return timer_id
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def end_timer(cls, timer_id: str, operation: str, **metadata) -> float:
|
|
56
|
+
"""End a timer and record the metric"""
|
|
57
|
+
if timer_id not in cls._active_timers:
|
|
58
|
+
log.warning(f"Timer {timer_id} not found")
|
|
59
|
+
return 0.0
|
|
60
|
+
|
|
61
|
+
start_time = cls._active_timers.pop(timer_id)
|
|
62
|
+
duration = time.perf_counter() - start_time
|
|
63
|
+
|
|
64
|
+
metric = TimingMetric(
|
|
65
|
+
operation=operation,
|
|
66
|
+
duration=duration,
|
|
67
|
+
**metadata
|
|
68
|
+
)
|
|
69
|
+
cls._metrics.append(metric)
|
|
70
|
+
|
|
71
|
+
log.debug(f"{operation}: {duration:.3f}s {metadata}")
|
|
72
|
+
return duration
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def record_timing(cls, operation: str, duration: float, **metadata):
|
|
76
|
+
"""Record a timing metric directly"""
|
|
77
|
+
metric = TimingMetric(
|
|
78
|
+
operation=operation,
|
|
79
|
+
duration=duration,
|
|
80
|
+
**metadata
|
|
81
|
+
)
|
|
82
|
+
cls._metrics.append(metric)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_metrics(cls) -> List[TimingMetric]:
|
|
86
|
+
"""Get all recorded metrics"""
|
|
87
|
+
return cls._metrics.copy()
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def get_metrics_by_operation(cls, operation: str) -> List[TimingMetric]:
|
|
91
|
+
"""Get metrics filtered by operation name"""
|
|
92
|
+
return [m for m in cls._metrics if m.operation == operation]
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def clear_metrics(cls):
|
|
96
|
+
"""Clear all recorded metrics"""
|
|
97
|
+
cls._metrics.clear()
|
|
98
|
+
cls._active_timers.clear()
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_summary_stats(cls, operation: str) -> Dict[str, float]:
|
|
102
|
+
"""Get summary statistics for an operation"""
|
|
103
|
+
metrics = cls.get_metrics_by_operation(operation)
|
|
104
|
+
if not metrics:
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
durations = [m.duration for m in metrics]
|
|
108
|
+
return {
|
|
109
|
+
'count': len(durations),
|
|
110
|
+
'total': sum(durations),
|
|
111
|
+
'mean': statistics.mean(durations),
|
|
112
|
+
'median': statistics.median(durations),
|
|
113
|
+
'min': min(durations),
|
|
114
|
+
'max': max(durations),
|
|
115
|
+
'std': statistics.stdev(durations) if len(durations) > 1 else 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def report_summary(cls, min_duration: float = 0.001) -> str:
|
|
120
|
+
"""Generate a summary report of all operations"""
|
|
121
|
+
operations = set(m.operation for m in cls._metrics)
|
|
122
|
+
|
|
123
|
+
report_lines = []
|
|
124
|
+
report_lines.append("=== Performance Summary Report ===")
|
|
125
|
+
report_lines.append(f"Total metrics recorded: {len(cls._metrics)}")
|
|
126
|
+
report_lines.append("")
|
|
127
|
+
|
|
128
|
+
# Group by operation
|
|
129
|
+
for operation in sorted(operations):
|
|
130
|
+
stats = cls.get_summary_stats(operation)
|
|
131
|
+
if stats['mean'] < min_duration: # Skip very fast operations
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
report_lines.append(f"{operation}:")
|
|
135
|
+
report_lines.append(f" Count: {stats['count']}")
|
|
136
|
+
report_lines.append(f" Total: {stats['total']:.3f}s")
|
|
137
|
+
report_lines.append(f" Mean: {stats['mean']:.3f}s ± {stats['std']:.3f}s")
|
|
138
|
+
report_lines.append(f" Range: {stats['min']:.3f}s - {stats['max']:.3f}s")
|
|
139
|
+
report_lines.append("")
|
|
140
|
+
|
|
141
|
+
report = '\n'.join(report_lines)
|
|
142
|
+
print(report)
|
|
143
|
+
return report
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def report_detailed(cls, operation: Optional[str] = None) -> str:
|
|
147
|
+
"""Generate detailed report showing individual measurements"""
|
|
148
|
+
metrics = cls.get_metrics_by_operation(operation) if operation else cls._metrics
|
|
149
|
+
|
|
150
|
+
report_lines = []
|
|
151
|
+
if operation:
|
|
152
|
+
report_lines.append(f"=== Detailed Report: {operation} ===")
|
|
153
|
+
else:
|
|
154
|
+
report_lines.append("=== Detailed Report: All Operations ===")
|
|
155
|
+
|
|
156
|
+
# Group by question if available
|
|
157
|
+
by_question = defaultdict(list)
|
|
158
|
+
for metric in metrics:
|
|
159
|
+
key = metric.question_name or "unknown"
|
|
160
|
+
by_question[key].append(metric)
|
|
161
|
+
|
|
162
|
+
for question_name, question_metrics in by_question.items():
|
|
163
|
+
report_lines.append(f"\nQuestion: {question_name}")
|
|
164
|
+
for metric in sorted(question_metrics, key=lambda m: m.variation_number or 0):
|
|
165
|
+
metadata_str = ""
|
|
166
|
+
if metric.question_type:
|
|
167
|
+
metadata_str += f" [{metric.question_type}]"
|
|
168
|
+
if metric.variation_number is not None:
|
|
169
|
+
metadata_str += f" var#{metric.variation_number}"
|
|
170
|
+
|
|
171
|
+
report_lines.append(f" {metric.operation}: {metric.duration:.3f}s{metadata_str}")
|
|
172
|
+
|
|
173
|
+
report = '\n'.join(report_lines)
|
|
174
|
+
print(report)
|
|
175
|
+
return report
|
|
176
|
+
|
|
177
|
+
@contextmanager
|
|
178
|
+
def timer(operation: str, **metadata):
|
|
179
|
+
"""Context manager for timing operations"""
|
|
180
|
+
timer_id = PerformanceTracker.start_timer(operation, **metadata)
|
|
181
|
+
try:
|
|
182
|
+
yield
|
|
183
|
+
finally:
|
|
184
|
+
PerformanceTracker.end_timer(timer_id, operation, **metadata)
|
|
185
|
+
|
|
186
|
+
def timed_method(operation_name: Optional[str] = None):
|
|
187
|
+
"""Decorator for timing method calls"""
|
|
188
|
+
def decorator(func):
|
|
189
|
+
def wrapper(*args, **kwargs):
|
|
190
|
+
op_name = operation_name or f"{func.__name__}"
|
|
191
|
+
|
|
192
|
+
# Try to extract question info from self if available
|
|
193
|
+
metadata = {}
|
|
194
|
+
if args and hasattr(args[0], 'name'):
|
|
195
|
+
metadata['question_name'] = getattr(args[0], 'name', None)
|
|
196
|
+
if args and hasattr(args[0], '__class__'):
|
|
197
|
+
metadata['question_type'] = args[0].__class__.__name__
|
|
198
|
+
|
|
199
|
+
with timer(op_name, **metadata):
|
|
200
|
+
return func(*args, **kwargs)
|
|
201
|
+
return wrapper
|
|
202
|
+
return decorator
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import List, Dict, Any, Tuple
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from QuizGenerator.contentast import *
|
|
9
|
+
from QuizGenerator.question import Question, QuestionRegistry, Answer
|
|
10
|
+
from QuizGenerator.mixins import TableQuestionMixin
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@QuestionRegistry.register()
|
|
16
|
+
class FromText(Question):
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, text, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.text = text
|
|
21
|
+
self.answers = []
|
|
22
|
+
self.possible_variations = 1
|
|
23
|
+
|
|
24
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
25
|
+
|
|
26
|
+
return ContentAST.Section([ContentAST.Text(self.text)])
|
|
27
|
+
|
|
28
|
+
def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
|
|
29
|
+
return Answer.AnswerKind.ESSAY, []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@QuestionRegistry.register()
|
|
33
|
+
class FromGenerator(FromText, TableQuestionMixin):
|
|
34
|
+
|
|
35
|
+
def __init__(self, *args, generator=None, text=None, **kwargs):
|
|
36
|
+
if generator is None and text is None:
|
|
37
|
+
raise TypeError(f"Must supply either generator or text kwarg for {self.__class__.__name__}")
|
|
38
|
+
|
|
39
|
+
if generator is None:
|
|
40
|
+
generator = text
|
|
41
|
+
|
|
42
|
+
super().__init__(*args, text="", **kwargs)
|
|
43
|
+
self.possible_variations = kwargs.get("possible_variations", float('inf'))
|
|
44
|
+
|
|
45
|
+
def attach_function_to_object(obj, function_code, function_name='get_body_lines'):
|
|
46
|
+
function_code = "import random\n" + function_code
|
|
47
|
+
|
|
48
|
+
# Create a local namespace for exec with ContentAST available
|
|
49
|
+
local_namespace = {
|
|
50
|
+
'ContentAST': ContentAST,
|
|
51
|
+
'Section': ContentAST.Section,
|
|
52
|
+
'Text': ContentAST.Text,
|
|
53
|
+
'Table': ContentAST.Table,
|
|
54
|
+
'Paragraph': ContentAST.Paragraph
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Define the function dynamically using exec
|
|
58
|
+
# Merge current globals with our local namespace for the exec
|
|
59
|
+
exec_globals = {**globals(), **local_namespace}
|
|
60
|
+
exec(f"def {function_name}(self):\n" + "\n".join(f" {line}" for line in function_code.splitlines()), exec_globals, local_namespace)
|
|
61
|
+
|
|
62
|
+
# Get the function and bind it to the object
|
|
63
|
+
function = local_namespace[function_name]
|
|
64
|
+
setattr(obj, function_name, function.__get__(obj))
|
|
65
|
+
|
|
66
|
+
self.generator_text = generator
|
|
67
|
+
# Attach the function dynamically
|
|
68
|
+
attach_function_to_object(self, generator, "generator")
|
|
69
|
+
|
|
70
|
+
self.answers = {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
74
|
+
return super().get_body()
|
|
75
|
+
|
|
76
|
+
def refresh(self, *args, **kwargs):
|
|
77
|
+
super().refresh(*args, **kwargs)
|
|
78
|
+
try:
|
|
79
|
+
generated_content = self.generator()
|
|
80
|
+
# Expect generator to return a ContentAST.Section or convert string to Section
|
|
81
|
+
if isinstance(generated_content, ContentAST.Section):
|
|
82
|
+
self.text = "" # Clear text since we'll override get_body
|
|
83
|
+
self._generated_section = generated_content
|
|
84
|
+
elif isinstance(generated_content, str):
|
|
85
|
+
self.text = generated_content
|
|
86
|
+
self._generated_section = None
|
|
87
|
+
else:
|
|
88
|
+
# Fallback
|
|
89
|
+
self.text = str(generated_content)
|
|
90
|
+
self._generated_section = None
|
|
91
|
+
except TypeError as e:
|
|
92
|
+
log.error(f"Error generating from text: {e}")
|
|
93
|
+
log.debug(self.generator_text)
|
|
94
|
+
exit(8)
|
|
95
|
+
|
|
96
|
+
def get_body(self, **kwargs) -> ContentAST.Section:
|
|
97
|
+
if hasattr(self, '_generated_section') and self._generated_section:
|
|
98
|
+
return self._generated_section
|
|
99
|
+
return super().get_body()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TrueFalse(FromText):
|
|
103
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CST334 Operating Systems questions
|