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,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