QuizGenerator 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Canvas LMS integration for QuizGenerator.
3
3
 
4
- Vendored from LMSInterface v0.1.0 (2025-11-18)
4
+ Vendored from LMSInterface v0.1.0 (2025-11-24)
5
5
 
6
6
  This module provides Canvas API integration for uploading quizzes
7
7
  and managing course content.
@@ -9,5 +9,5 @@ and managing course content.
9
9
 
10
10
  __version__ = "0.1.0"
11
11
  __vendored_from__ = "LMSInterface"
12
- __vendored_date__ = "2025-11-18"
12
+ __vendored_date__ = "2025-11-24"
13
13
 
@@ -159,7 +159,12 @@ class CanvasCourse(LMSWrapper):
159
159
 
160
160
  question_fingerprint = question_for_canvas["question_text"]
161
161
  try:
162
- question_fingerprint += ''.join([str(a["answer_text"]) for a in question_for_canvas["answers"]])
162
+ question_fingerprint += ''.join([
163
+ '|'.join([
164
+ f"{k}:{a[k]}" for k in sorted(a.keys())
165
+ ])
166
+ for a in question_for_canvas["answers"]
167
+ ])
163
168
  except TypeError as e:
164
169
  log.error(e)
165
170
  log.warning("Continuing anyway")
@@ -75,7 +75,10 @@ class ContentAST:
75
75
  html_output = section.render("html")
76
76
  """
77
77
  def __init__(self, elements=None, add_spacing_before=False):
78
- self.elements : List[ContentAST.Element] = elements or []
78
+ self.elements : List[ContentAST.Element] = [
79
+ e if isinstance(e, ContentAST.Element) else ContentAST.Text(e)
80
+ for e in (elements if elements else [])
81
+ ]
79
82
  self.add_spacing_before = add_spacing_before
80
83
 
81
84
  def __str__(self):
@@ -436,7 +439,8 @@ class ContentAST:
436
439
  interest=1.0,
437
440
  spacing=0,
438
441
  topic=None,
439
- question_number=None
442
+ question_number=None,
443
+ **kwargs
440
444
  ):
441
445
  super().__init__()
442
446
  self.name = name
@@ -447,14 +451,21 @@ class ContentAST:
447
451
  self.spacing = spacing
448
452
  self.topic = topic # todo: remove this bs.
449
453
  self.question_number = question_number # For QR code generation
454
+
455
+ self.default_kwargs = kwargs
450
456
 
451
457
  def render(self, output_format, **kwargs):
458
+ updated_kwargs = self.default_kwargs
459
+ updated_kwargs.update(kwargs)
460
+
461
+ log.debug(f"updated_kwargs: {updated_kwargs}")
462
+
452
463
  # Special handling for latex and typst - use dedicated render methods
453
464
  if output_format == "typst":
454
465
  return self.render_typst(**kwargs)
455
466
 
456
467
  # Generate content from all elements
457
- content = self.body.render(output_format, **kwargs)
468
+ content = self.body.render(output_format, **updated_kwargs)
458
469
 
459
470
  # If output format is latex, add in minipage and question environments
460
471
  if output_format == "latex":
@@ -1420,7 +1431,11 @@ class ContentAST:
1420
1431
  key_to_display = self.answer[0].key
1421
1432
  return f"{self.label + (':' if len(self.label) > 0 else '')} [{key_to_display}] {self.unit}".strip()
1422
1433
 
1423
- def render_html(self, show_answers=False, **kwargs):
1434
+ def render_html(self, show_answers=False, can_be_numerical=False, **kwargs):
1435
+ log.debug(f"can_be_numerical: {can_be_numerical}")
1436
+ log.debug(f"kwargs: {kwargs}")
1437
+ if can_be_numerical:
1438
+ return f"Calculate {self.label}"
1424
1439
  if show_answers and self.answer:
1425
1440
  # Show actual answer value using formatted display string
1426
1441
  if not isinstance(self.answer, list):
QuizGenerator/generate.py CHANGED
@@ -25,6 +25,8 @@ def parse_args():
25
25
  default=os.path.join(Path.home(), '.env'),
26
26
  help="Path to .env file specifying canvas details"
27
27
  )
28
+
29
+ parser.add_argument("--debug", action="store_true", help="Set logging level to debug")
28
30
 
29
31
  parser.add_argument("--quiz_yaml", default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "example_files/exam_generation.yaml"))
30
32
  parser.add_argument("--seed", type=int, default=None,
@@ -211,6 +213,21 @@ def main():
211
213
 
212
214
  # Load environment variables
213
215
  load_dotenv(args.env)
216
+
217
+ if args.debug:
218
+ # Set root logger to DEBUG
219
+ logging.getLogger().setLevel(logging.DEBUG)
220
+
221
+ # Set all handlers to DEBUG level
222
+ for handler in logging.getLogger().handlers:
223
+ handler.setLevel(logging.DEBUG)
224
+
225
+ # Set named loggers to DEBUG
226
+ for logger_name in ['QuizGenerator', 'lms_interface', '__main__']:
227
+ logger = logging.getLogger(logger_name)
228
+ logger.setLevel(logging.DEBUG)
229
+ for handler in logger.handlers:
230
+ handler.setLevel(logging.DEBUG)
214
231
 
215
232
  if args.command == "TEST":
216
233
  test()
@@ -233,4 +250,4 @@ def main():
233
250
 
234
251
 
235
252
  if __name__ == "__main__":
236
- main()
253
+ main()
QuizGenerator/misc.py CHANGED
@@ -6,7 +6,7 @@ import enum
6
6
  import itertools
7
7
  import logging
8
8
  import math
9
- from typing import List, Dict, Tuple
9
+ from typing import List, Dict, Tuple, Any
10
10
 
11
11
  import fractions
12
12
 
@@ -23,11 +23,12 @@ class Answer:
23
23
 
24
24
  class AnswerKind(enum.Enum):
25
25
  BLANK = "fill_in_multiple_blanks_question"
26
- MULTIPLE_ANSWER = "multiple_answers_question" # todo: have baffles?
26
+ MULTIPLE_ANSWER = "multiple_answers_question"
27
27
  ESSAY = "essay_question"
28
28
  MULTIPLE_DROPDOWN = "multiple_dropdowns_question"
29
+ NUMERICAL_QUESTION = "numerical_question" # note: these can only be single answers as far as I can tell
29
30
 
30
- class VariableKind(enum.Enum): # todo: use these for generate variations?
31
+ class VariableKind(enum.Enum):
31
32
  STR = enum.auto()
32
33
  INT = enum.auto()
33
34
  FLOAT = enum.auto()
@@ -66,7 +67,7 @@ class Answer:
66
67
  self.baffles = baffles
67
68
  self.pdf_only = pdf_only
68
69
 
69
- def get_for_canvas(self) -> List[Dict]:
70
+ def get_for_canvas(self, single_answer=False) -> List[Dict]:
70
71
  # If this answer is marked as PDF-only, don't send it to Canvas
71
72
  if self.pdf_only:
72
73
  return []
@@ -131,18 +132,29 @@ class Answer:
131
132
  Answer.VariableKind.FLOAT,
132
133
  Answer.VariableKind.INT
133
134
  ]:
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 = [
135
+ if single_answer:
136
+ canvas_answers = [
137
+ {
138
+ "numerical_answer_type": "exact_answer",
139
+ "answer_text": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
140
+ "answer_exact": round(self.value, self.DEFAULT_ROUNDING_DIGITS),
141
+ "answer_error_margin": 0.1,
142
+ "answer_weight": 100 if self.correct else 0,
143
+ }
144
+ ]
145
+ else:
146
+ # Use the accepted_strings helper with settings that match the original AUTOFLOAT behavior
147
+ answer_strings = self.__class__.accepted_strings(
148
+ self.value,
149
+ allow_integer=True,
150
+ allow_simple_fraction=True,
151
+ max_denominator=3*4*5, # For process questions, these are the numbers of jobs we'd have
152
+ allow_mixed=True,
153
+ include_spaces=False,
154
+ include_fixed_even_if_integer=True
155
+ )
156
+
157
+ canvas_answers = [
146
158
  {
147
159
  "blank_id": self.key,
148
160
  "answer_text": answer_string,
@@ -476,5 +488,4 @@ class Answer:
476
488
  whole, rem = divmod(A, b)
477
489
  outs.add(f"{sign}{whole} {rem}/{b}")
478
490
 
479
- return sorted(outs, key=lambda s: (len(s), s))
480
-
491
+ return sorted(outs, key=lambda s: (len(s), s))
@@ -190,6 +190,8 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
190
190
  self.num_elements = self.config_params.get("num_elements", 5)
191
191
  self.cache_size = self.config_params.get("cache_size", 3)
192
192
  self.num_requests = self.config_params.get("num_requests", 10)
193
+
194
+ self.hit_rate = 0. # placeholder
193
195
 
194
196
  def refresh(self, previous: Optional[CachingQuestion] = None, *args, hard_refresh: bool = False, **kwargs):
195
197
  # Call parent refresh which seeds RNG and calls is_interesting()
@@ -200,7 +202,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
200
202
  self.cache_policy = self.get_choice('policy', self.Kind)
201
203
 
202
204
  self.requests = (
203
- list(range(self.cache_size)) # Prime the cache with the compulsory misses
205
+ list(range(self.cache_size)) # Prime the cache with the capacity misses
204
206
  + self.rng.choices(
205
207
  population=list(range(self.cache_size - 1)), k=1
206
208
  ) # Add in one request to an earlier that will differentiate clearly between FIFO and LRU
@@ -274,7 +276,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
274
276
  hit_rate_block = ContentAST.AnswerBlock(
275
277
  ContentAST.Answer(
276
278
  answer=self.answers["answer__hit_rate"],
277
- label=f"Hit rate, excluding compulsory misses. If appropriate, round to {Answer.DEFAULT_ROUNDING_DIGITS} decimal digits.",
279
+ label=f"Hit rate, excluding capacity misses. If appropriate, round to {Answer.DEFAULT_ROUNDING_DIGITS} decimal digits.",
278
280
  unit="%"
279
281
  )
280
282
  )
@@ -324,7 +326,7 @@ class CachingQuestion(MemoryQuestion, RegenerableChoiceMixin, TableQuestionMixin
324
326
  "To calculate the hit rate we calculate the percentage of requests "
325
327
  "that were cache hits out of the total number of requests. "
326
328
  f"In this case we are counting only all but {self.cache_size} requests, "
327
- f"since we are excluding compulsory misses."
329
+ f"since we are excluding capacity misses."
328
330
  ]
329
331
  )
330
332
  )
QuizGenerator/question.py CHANGED
@@ -11,6 +11,7 @@ import itertools
11
11
  import os
12
12
  import pathlib
13
13
  import pkgutil
14
+ import pprint
14
15
  import random
15
16
  import re
16
17
  import uuid
@@ -522,7 +523,9 @@ class Question(abc.ABC):
522
523
  explanation=explanation,
523
524
  value=self.points_value,
524
525
  spacing=self.spacing,
525
- topic=self.topic
526
+ topic=self.topic,
527
+
528
+ can_be_numerical=self.can_be_numerical()
526
529
  )
527
530
 
528
531
  # Attach regeneration metadata to the question AST
@@ -534,7 +537,7 @@ class Question(abc.ABC):
534
537
  question_ast.config_params = dict(self.config_params)
535
538
 
536
539
  return question_ast
537
-
540
+
538
541
  @abc.abstractmethod
539
542
  def get_body(self, **kwargs) -> ContentAST.Section:
540
543
  """
@@ -555,11 +558,20 @@ class Question(abc.ABC):
555
558
  )
556
559
 
557
560
  def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
561
+ if self.can_be_numerical():
562
+ return (
563
+ Answer.AnswerKind.NUMERICAL_QUESTION,
564
+ list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
565
+ )
566
+ elif len(self.answers.values()) > 0:
567
+ return (
568
+ self.answer_kind,
569
+ list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
570
+ )
558
571
  return (
559
- self.answer_kind,
560
- list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
572
+ Answer.AnswerKind.ESSAY, []
561
573
  )
562
-
574
+
563
575
  def refresh(self, rng_seed=None, *args, **kwargs):
564
576
  """If it is necessary to regenerate aspects between usages, this is the time to do it.
565
577
  This base implementation simply resets everything.
@@ -621,7 +633,13 @@ class Question(abc.ABC):
621
633
  "answers": answers,
622
634
  "neutral_comments_html": explanation_html
623
635
  }
624
-
636
+
637
+ def can_be_numerical(self):
638
+ if (len(self.answers.values()) == 1
639
+ and list(self.answers.values())[0].variable_kind in [Answer.VariableKind.FLOAT, Answer.VariableKind.AUTOFLOAT]
640
+ ):
641
+ return True
642
+ return False
625
643
 
626
644
  class QuestionGroup():
627
645
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
5
5
  Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
6
6
  Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
@@ -2,25 +2,25 @@ QuizGenerator/README.md,sha256=4n16gKyhIAKRBX4VKlpfcK0pyUYJ6Ht08MUsnwgxrZo,145
2
2
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
3
3
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
4
4
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
5
- QuizGenerator/contentast.py,sha256=bHC1D_OBrDCnTuXcTiI37Muk20mBxD1gjDArUBrGv9c,63657
6
- QuizGenerator/generate.py,sha256=HK8TtfQwkj59n0_BVSrej_CuQUnDwoIGlqDikI4YwK4,6613
5
+ QuizGenerator/contentast.py,sha256=3yvgtpw4ZSN9Cxfiq7QncT0JMoLoX4XuQ57GeIkfRj8,64171
6
+ QuizGenerator/generate.py,sha256=o2XezoSE0u-qjxYu1_Ofm9Lpkza7M2Tg47C-ClMcPsE,7197
7
7
  QuizGenerator/logging.yaml,sha256=VJCdh26D8e_PNUs4McvvP1ojz9EVjQNifJzfhEk1Mbo,1114
8
- QuizGenerator/misc.py,sha256=uIc1revyGZK8LL-1sJfhsoJ_MlYn75LPap6FRAiJWRQ,15635
8
+ QuizGenerator/misc.py,sha256=HE6B49IQ4E2EdAIO2yprlluVRS5dtWtFsiIkk68zY2E,16125
9
9
  QuizGenerator/mixins.py,sha256=RjV76C1tkTLSvhSoMys67W7UmR6y6wAAcBM2Msxdpd0,18186
10
10
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
11
11
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
12
- QuizGenerator/question.py,sha256=ZA-Yl28mtBXV3GgUipqogL9pKEBiLYzHq_io9wLXcKo,25390
12
+ QuizGenerator/question.py,sha256=c72zJMoUGH-Io7XVaR1kLBzlW_s-0W0_XeMy9gbIuIg,26004
13
13
  QuizGenerator/quiz.py,sha256=wOuj_mDfdnUNHlAEAcLUZoH-JX57RovLoYW4mA0GgPc,18762
14
14
  QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
15
- QuizGenerator/canvas/__init__.py,sha256=qsgHE4cz_WBXYZ3DZ_fEqLXUKbm-fH6oLkNUAnX96HY,286
16
- QuizGenerator/canvas/canvas_interface.py,sha256=vbfLX9BBKIOwkVyiXzrwIsjdRbl6jYTnxznt-ymARrM,24407
15
+ QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
16
+ QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
17
17
  QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
18
18
  QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  QuizGenerator/premade_questions/basic.py,sha256=wAvVZED6a7VToIvSCdAx6SrExmR0xVRo5dL40kycdXI,3402
20
20
  QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
21
21
  QuizGenerator/premade_questions/cst334/languages.py,sha256=N5vcmZi-AFM_BZypnvNogHD7s--28-j-8tykYg6CBzs,14388
22
22
  QuizGenerator/premade_questions/cst334/math_questions.py,sha256=za8lNqhM0RB8qefmPP-Ww0WB_SQn0iRcBKOrZgyHCQQ,9290
23
- QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=s887hyUyOBcUa53zhPxwdOV2YbJChSywZNpTtAqeljs,51561
23
+ QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=QhCALX7nzq_moLnkok4JVNO4crtmJnosg7_xOZL_5-c,51597
24
24
  QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
25
25
  QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=em-HzFRnaroDmHl5uA771HyFMI7dMvG-gxTgsB3ecaY,14458
26
26
  QuizGenerator/premade_questions/cst334/process.py,sha256=C7-FzOGW7EzQHXZNKden7eHo1NoG0UQbci0ZrUtybzs,23732
@@ -37,8 +37,8 @@ QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=
37
37
  QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=7ZtCQ2fXhIPSd99TstQfOKCN13GJE_56UfBQKdmzmMI,42398
38
38
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
39
39
  QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=dPn8Sj0yk4m02np62esMKZ7CvcljhYq3Tq51nY9aJnA,29781
40
- quizgenerator-0.1.2.dist-info/METADATA,sha256=GpIkM0qosTZY2p0NFI7K1Zg3g33mUOTuHtlz82awGvI,7149
41
- quizgenerator-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
- quizgenerator-0.1.2.dist-info/entry_points.txt,sha256=iViWMzswXGe88WKoue_Ib-ODUSiT_j_6f1us28w9pkc,56
43
- quizgenerator-0.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
44
- quizgenerator-0.1.2.dist-info/RECORD,,
40
+ quizgenerator-0.1.4.dist-info/METADATA,sha256=ebekoIHR35xPw-W6sVVHxD1xQulJioslbCYtT8uCfiE,7149
41
+ quizgenerator-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
42
+ quizgenerator-0.1.4.dist-info/entry_points.txt,sha256=iViWMzswXGe88WKoue_Ib-ODUSiT_j_6f1us28w9pkc,56
43
+ quizgenerator-0.1.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
44
+ quizgenerator-0.1.4.dist-info/RECORD,,