QuizGenerator 0.4.2__py3-none-any.whl → 0.6.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 (33) hide show
  1. QuizGenerator/contentast.py +809 -117
  2. QuizGenerator/generate.py +219 -11
  3. QuizGenerator/misc.py +0 -556
  4. QuizGenerator/mixins.py +50 -29
  5. QuizGenerator/premade_questions/basic.py +3 -3
  6. QuizGenerator/premade_questions/cst334/languages.py +183 -175
  7. QuizGenerator/premade_questions/cst334/math_questions.py +81 -70
  8. QuizGenerator/premade_questions/cst334/memory_questions.py +262 -165
  9. QuizGenerator/premade_questions/cst334/persistence_questions.py +83 -60
  10. QuizGenerator/premade_questions/cst334/process.py +558 -79
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +39 -13
  12. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +61 -36
  13. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +29 -10
  14. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  15. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +60 -43
  16. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +173 -326
  17. QuizGenerator/premade_questions/cst463/models/attention.py +29 -14
  18. QuizGenerator/premade_questions/cst463/models/cnns.py +32 -20
  19. QuizGenerator/premade_questions/cst463/models/rnns.py +28 -15
  20. QuizGenerator/premade_questions/cst463/models/text.py +29 -15
  21. QuizGenerator/premade_questions/cst463/models/weight_counting.py +38 -30
  22. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +91 -111
  23. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +128 -55
  24. QuizGenerator/question.py +114 -20
  25. QuizGenerator/quiz.py +81 -24
  26. QuizGenerator/regenerate.py +98 -29
  27. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/RECORD +31 -33
  29. QuizGenerator/README.md +0 -5
  30. QuizGenerator/logging.yaml +0 -55
  31. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/WHEEL +0 -0
  32. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/entry_points.txt +0 -0
  33. {quizgenerator-0.4.2.dist-info → quizgenerator-0.6.0.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/question.py CHANGED
@@ -21,14 +21,21 @@ import yaml
21
21
  from typing import List, Dict, Any, Tuple, Optional
22
22
  import canvasapi.course, canvasapi.quiz
23
23
 
24
- from QuizGenerator.misc import Answer
25
- from QuizGenerator.contentast import ContentAST
24
+ from QuizGenerator.contentast import ContentAST, AnswerTypes
26
25
  from QuizGenerator.performance import timer, PerformanceTracker
27
26
 
28
27
  import logging
29
28
  log = logging.getLogger(__name__)
30
29
 
31
30
 
31
+ @dataclasses.dataclass
32
+ class QuestionComponents:
33
+ """Bundle of question parts generated during construction."""
34
+ body: ContentAST.Element
35
+ answers: List[ContentAST.Answer]
36
+ explanation: ContentAST.Element
37
+
38
+
32
39
  # Spacing presets for questions
33
40
  SPACING_PRESETS = {
34
41
  "NONE": 0,
@@ -344,25 +351,31 @@ class Question(abc.ABC):
344
351
  ensures consistent rendering across PDF/LaTeX and Canvas/HTML formats.
345
352
 
346
353
  Required Methods:
347
- - get_body(): Return ContentAST.Section with question content
348
- - get_explanation(): Return ContentAST.Section with solution steps
354
+ - _get_body(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with body and answers
355
+ - _get_explanation(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with explanation
356
+
357
+ Note: get_body() and get_explanation() are provided for backward compatibility
358
+ and call the _get_* methods, returning just the first element of the tuple.
349
359
 
350
360
  Required Class Attributes:
351
361
  - VERSION (str): Question version number (e.g., "1.0")
352
362
  Increment when RNG logic changes to ensure reproducibility
353
363
 
354
364
  ContentAST Usage Examples:
355
- def get_body(self):
365
+ def _get_body(self):
356
366
  body = ContentAST.Section()
367
+ answers = []
357
368
  body.add_element(ContentAST.Paragraph(["Calculate the matrix:"]))
358
369
 
359
370
  # Use ContentAST.Matrix for math, NOT manual LaTeX
360
371
  matrix_data = [[1, 2], [3, 4]]
361
372
  body.add_element(ContentAST.Matrix(data=matrix_data, bracket_type="b"))
362
373
 
363
- # Use ContentAST.Answer for input fields
364
- body.add_element(ContentAST.Answer(answer=self.answers["result"]))
365
- return body
374
+ # Answer extends ContentAST.Leaf - add directly to body
375
+ ans = ContentAST.Answer.integer("result", 42, label="Result")
376
+ answers.append(ans)
377
+ body.add_element(ans)
378
+ return body, answers
366
379
 
367
380
  Common ContentAST Elements:
368
381
  - ContentAST.Paragraph: Text blocks
@@ -460,7 +473,7 @@ class Question(abc.ABC):
460
473
  self.points_value = points_value
461
474
  self.topic = topic
462
475
  self.spacing = parse_spacing(kwargs.get("spacing", 0))
463
- self.answer_kind = Answer.AnswerKind.BLANK
476
+ self.answer_kind = ContentAST.Answer.CanvasAnswerKind.BLANK
464
477
 
465
478
  # Support for multi-part questions (defaults to 1 for normal questions)
466
479
  self.num_subquestions = kwargs.get("num_subquestions", 1)
@@ -472,6 +485,9 @@ class Question(abc.ABC):
472
485
 
473
486
  self.rng_seed_offset = kwargs.get("rng_seed_offset", 0)
474
487
 
488
+ # Component caching for unified Answer architecture
489
+ self._components: QuestionComponents = None
490
+
475
491
  # To be used throughout when generating random things
476
492
  self.rng = random.Random()
477
493
 
@@ -569,28 +585,105 @@ class Question(abc.ABC):
569
585
 
570
586
  def get_explanation(self, **kwargs) -> ContentAST.Section:
571
587
  """
572
- Gets the body of the question during generation
588
+ Gets the body of the question during generation (backward compatible wrapper).
589
+ Calls _get_explanation() and returns just the explanation.
573
590
  :param kwargs:
574
591
  :return: (ContentAST.Section) Containing question explanation or None
575
592
  """
593
+ # Try new pattern first
594
+ if hasattr(self, '_get_explanation') and callable(getattr(self, '_get_explanation')):
595
+ explanation, _ = self._get_explanation()
596
+ return explanation
597
+ # Fallback: default explanation
576
598
  return ContentAST.Section(
577
599
  [ContentAST.Text("[Please reach out to your professor for clarification]")]
578
600
  )
579
-
580
- def get_answers(self, *args, **kwargs) -> Tuple[Answer.AnswerKind, List[Dict[str,Any]]]:
581
- if self.can_be_numerical():
601
+
602
+ def _get_body(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
603
+ """
604
+ Build question body and collect answers (new pattern).
605
+ Questions should override this to return (body, answers) tuple.
606
+
607
+ Returns:
608
+ Tuple of (body_ast, answers_list)
609
+ """
610
+ # Fallback: call old get_body() and return empty answers
611
+ body = self.get_body()
612
+ return body, []
613
+
614
+ def _get_explanation(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
615
+ """
616
+ Build question explanation and collect answers (new pattern).
617
+ Questions can override this to include answers in explanations.
618
+
619
+ Returns:
620
+ Tuple of (explanation_ast, answers_list)
621
+ """
622
+ return ContentAST.Section(
623
+ [ContentAST.Text("[Please reach out to your professor for clarification]")]
624
+ ), []
625
+
626
+ def build_question_components(self, **kwargs) -> QuestionComponents:
627
+ """
628
+ Build question components (body, answers, explanation) in single pass.
629
+
630
+ Calls _get_body() and _get_explanation() which return tuples of
631
+ (content, answers).
632
+ """
633
+ # Build body with its answers
634
+ body, body_answers = self._get_body()
635
+
636
+ # Build explanation with its answers
637
+ explanation, explanation_answers = self._get_explanation()
638
+
639
+ # Combine all answers
640
+ all_answers = body_answers + explanation_answers
641
+
642
+ return QuestionComponents(
643
+ body=body,
644
+ answers=all_answers,
645
+ explanation=explanation
646
+ )
647
+
648
+ def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
649
+ """
650
+ Return answers from cached components (new pattern) or self.answers dict (old pattern).
651
+ """
652
+ # Try component-based approach first (new pattern)
653
+ if self._components is None:
654
+ try:
655
+ self._components = self.build_question_components()
656
+ except Exception as e:
657
+ # If component building fails, fall back to dict
658
+ log.debug(f"Failed to build question components: {e}, falling back to dict")
659
+ pass
660
+
661
+ # Use components if available and non-empty
662
+ if self._components is not None and len(self._components.answers) > 0:
663
+ answers = self._components.answers
664
+ if self.can_be_numerical():
665
+ return (
666
+ ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
667
+ list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in answers]))
668
+ )
582
669
  return (
583
- Answer.AnswerKind.NUMERICAL_QUESTION,
584
- list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
670
+ self.answer_kind,
671
+ list(itertools.chain(*[a.get_for_canvas() for a in answers]))
585
672
  )
586
- elif len(self.answers.values()) > 0:
673
+
674
+ # Fall back to dict pattern (old pattern)
675
+ if len(self.answers.values()) > 0:
676
+ if self.can_be_numerical():
677
+ return (
678
+ ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
679
+ list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
680
+ )
587
681
  return (
588
682
  self.answer_kind,
589
683
  list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
590
684
  )
591
- return (
592
- Answer.AnswerKind.ESSAY, []
593
- )
685
+
686
+ return (ContentAST.Answer.CanvasAnswerKind.ESSAY, [])
594
687
 
595
688
  def refresh(self, rng_seed=None, *args, **kwargs):
596
689
  """If it is necessary to regenerate aspects between usages, this is the time to do it.
@@ -601,6 +694,7 @@ class Question(abc.ABC):
601
694
  :return: bool - True if the generated question is interesting, False otherwise
602
695
  """
603
696
  self.answers = {}
697
+ self._components = None # Clear component cache
604
698
  # Seed the RNG directly with the provided seed (no offset)
605
699
  self.rng.seed(rng_seed)
606
700
  # Note: We don't call is_interesting() here because child classes need to
@@ -656,7 +750,7 @@ class Question(abc.ABC):
656
750
 
657
751
  def can_be_numerical(self):
658
752
  if (len(self.answers.values()) == 1
659
- and list(self.answers.values())[0].variable_kind in [Answer.VariableKind.FLOAT, Answer.VariableKind.AUTOFLOAT]
753
+ and isinstance(list(self.answers.values())[0], AnswerTypes.Float)
660
754
  ):
661
755
  return True
662
756
  return False
QuizGenerator/quiz.py CHANGED
@@ -11,6 +11,7 @@ import subprocess
11
11
  import tempfile
12
12
  from datetime import datetime
13
13
  from typing import List, Dict, Optional
14
+ import re
14
15
 
15
16
  import yaml
16
17
 
@@ -87,6 +88,11 @@ class Quiz:
87
88
 
88
89
  # Get general quiz information from the dictionary
89
90
  name = exam_dict.get("name", f"Unnamed Exam ({datetime.now().strftime('%a %b %d %I:%M %p')})")
91
+ if isinstance(name, str):
92
+ def replace_time(match: re.Match) -> str:
93
+ fmt = match.group(1) or "%b %d %I:%M%p"
94
+ return datetime.now().strftime(fmt)
95
+ name = re.sub(r"\$TIME(?:\{([^}]+)\})?", replace_time, name)
90
96
  practice = exam_dict.get("practice", False)
91
97
  description = exam_dict.get("description", None)
92
98
  sort_order = list(map(lambda t: Question.Topic.from_string(t), exam_dict.get("sort order", [])))
@@ -275,10 +281,29 @@ class Quiz:
275
281
 
276
282
  # For each point group, estimate heights and apply bin-packing optimization
277
283
  optimized_questions = []
278
- is_first_page = True # Track if we're packing the first page
284
+ is_first_bin_overall = True # Track if we're packing the very first bin of the entire exam
279
285
 
280
286
  log.debug("Optimizing question order for PDF layout...")
281
287
 
288
+ def get_spacing_priority(question):
289
+ """
290
+ Get placement priority based on spacing. Lower values = higher priority.
291
+ Order: LONG (9), MEDIUM (6), SHORT (4), NONE (0), then PAGE (99), EXTRA_PAGE (199)
292
+ """
293
+ spacing = question.spacing
294
+ if spacing >= 199: # EXTRA_PAGE
295
+ return 5
296
+ elif spacing >= 99: # PAGE
297
+ return 4
298
+ elif spacing >= 9: # LONG
299
+ return 0
300
+ elif spacing >= 6: # MEDIUM
301
+ return 1
302
+ elif spacing >= 4: # SHORT
303
+ return 2
304
+ else: # NONE or custom small values
305
+ return 3
306
+
282
307
  for points in sorted(point_groups.keys(), reverse=True):
283
308
  group = point_groups[points]
284
309
 
@@ -288,27 +313,30 @@ class Quiz:
288
313
  group.sort(key=lambda q: self.question_sort_order.index(q.topic))
289
314
  optimized_questions.extend(group)
290
315
  log.debug(f" {points}pt questions: {len(group)} questions (order preserved by config)")
291
- # After adding preserved-order questions, we're likely past the first page
292
- is_first_page = False
316
+ # After adding preserved-order questions, we're no longer on the first bin
317
+ is_first_bin_overall = False
293
318
  continue
294
319
 
295
320
  # If only 1-2 questions, no optimization needed
296
321
  if len(group) <= 2:
297
- # Still sort by topic for consistency
298
- group.sort(key=lambda q: self.question_sort_order.index(q.topic))
322
+ # Sort by spacing priority first, then topic
323
+ group.sort(key=lambda q: (get_spacing_priority(q), self.question_sort_order.index(q.topic)))
299
324
  optimized_questions.extend(group)
300
325
  log.debug(f" {points}pt questions: {len(group)} questions (no optimization needed)")
301
- is_first_page = False
326
+ is_first_bin_overall = False
302
327
  continue
303
328
 
304
- # Estimate height for each question
305
- question_heights = [(q, self._estimate_question_height(q, **kwargs)) for q in group]
329
+ # Estimate height for each question, preserving original index for stable sorting
330
+ question_heights = [(i, q, self._estimate_question_height(q, **kwargs)) for i, q in enumerate(group)]
306
331
 
307
- # Sort by height descending to identify large and small questions
308
- question_heights.sort(key=lambda x: x[1], reverse=True)
332
+ # Sort by:
333
+ # 1. Spacing priority (LONG, MEDIUM, SHORT, NONE, then PAGE, EXTRA_PAGE)
334
+ # 2. Height descending (within same spacing category)
335
+ # 3. Original index (for deterministic ordering)
336
+ question_heights.sort(key=lambda x: (get_spacing_priority(x[1]), -x[2], x[0]))
309
337
 
310
338
  log.debug(f" Question heights for {points}pt questions:")
311
- for q, h in question_heights:
339
+ for idx, q, h in question_heights:
312
340
  log.debug(f" {q.name}: {h:.1f}cm (spacing={q.spacing}cm)")
313
341
 
314
342
  # Calculate page capacity in centimeters
@@ -317,7 +345,7 @@ class Quiz:
317
345
  base_page_capacity = 22.0 # cm
318
346
 
319
347
  # First page has header (title + name line) which takes ~3cm
320
- first_page_capacity = base_page_capacity - 3.0 if is_first_page else base_page_capacity
348
+ first_page_capacity = base_page_capacity - 3.0 # cm
321
349
 
322
350
  # Better bin-packing strategy: interleave large and small questions
323
351
  # Strategy: Start each page with the largest unplaced question, then fill with smaller ones
@@ -326,21 +354,48 @@ class Quiz:
326
354
 
327
355
  while not all(placed):
328
356
  # Determine capacity for this page
329
- page_capacity = first_page_capacity if len(bins) == 0 and is_first_page else base_page_capacity
357
+ # Use first_page_capacity only for the very first bin of the entire exam
358
+ page_capacity = first_page_capacity if (len(bins) == 0 and is_first_bin_overall) else base_page_capacity
330
359
 
331
360
  # Find the largest unplaced question to start a new page
332
361
  new_page = []
333
362
  page_height = 0
334
363
 
335
- for i, (question, height) in enumerate(question_heights):
336
- if not placed[i]:
337
- new_page.append(question)
338
- page_height = height
339
- placed[i] = True
340
- break
364
+ # Special handling for first bin of entire exam: avoid questions with PAGE spacing (99+cm)
365
+ # to prevent them from pushing content to page 2
366
+ if len(bins) == 0 and is_first_bin_overall:
367
+ # Try to find a question without PAGE/EXTRA_PAGE spacing for the first page
368
+ # PAGE=99cm, EXTRA_PAGE=199cm - these need full pages
369
+ found_non_page_question = False
370
+ for i, (idx, question, height) in enumerate(question_heights):
371
+ if not placed[i] and question.spacing < 99:
372
+ new_page.append(question)
373
+ page_height = height
374
+ placed[i] = True
375
+ found_non_page_question = True
376
+ log.debug(f" First bin (page 1): Selected {question.name} with spacing={question.spacing}cm")
377
+ break
378
+
379
+ # If all questions have PAGE spacing, fall back to normal behavior (use largest question)
380
+ if not found_non_page_question:
381
+ for i, (idx, question, height) in enumerate(question_heights):
382
+ if not placed[i]:
383
+ new_page.append(question)
384
+ page_height = height
385
+ placed[i] = True
386
+ log.debug(f" First bin (page 1): All questions have PAGE spacing, using {question.name} (spacing={question.spacing}cm)")
387
+ break
388
+ else:
389
+ # Normal behavior for non-first pages
390
+ for i, (idx, question, height) in enumerate(question_heights):
391
+ if not placed[i]:
392
+ new_page.append(question)
393
+ page_height = height
394
+ placed[i] = True
395
+ break
341
396
 
342
397
  # Now try to fill the remaining space with smaller questions
343
- for i, (question, height) in enumerate(question_heights):
398
+ for i, (idx, question, height) in enumerate(question_heights):
344
399
  if not placed[i] and page_height + height <= page_capacity:
345
400
  new_page.append(question)
346
401
  page_height += height
@@ -348,6 +403,11 @@ class Quiz:
348
403
 
349
404
  bins.append((new_page, page_height))
350
405
 
406
+ # After creating the first bin, we're no longer on the first page
407
+ if len(bins) == 1 and is_first_bin_overall:
408
+ is_first_bin_overall = False
409
+ log.debug(f" First bin created, subsequent bins will use normal page capacity")
410
+
351
411
  log.debug(f" {points}pt questions: {len(group)} questions packed into {len(bins)} pages")
352
412
  for i, (page_questions, height) in enumerate(bins):
353
413
  log.debug(f" Page {i+1}: {height:.1f}cm with {len(page_questions)} questions: {[q.name for q in page_questions]}")
@@ -356,9 +416,6 @@ class Quiz:
356
416
  for bin_contents, _ in bins:
357
417
  optimized_questions.extend(bin_contents)
358
418
 
359
- # After packing questions, we're no longer on the first page
360
- is_first_page = False
361
-
362
419
  return optimized_questions
363
420
 
364
421
  def get_quiz(self, **kwargs) -> ContentAST.Document:
@@ -464,4 +521,4 @@ def main():
464
521
 
465
522
  if __name__ == "__main__":
466
523
  main()
467
-
524
+
@@ -13,6 +13,12 @@ CLI Usage:
13
13
  # Scan QR codes from a scanned exam page
14
14
  python grade_from_qr.py --image exam_page.jpg --all
15
15
 
16
+ # Decode an encrypted string directly
17
+ python -m QuizGenerator.regenerate --encrypted_str "EzE6JF86CDlf..."
18
+
19
+ # Decode with custom point value
20
+ python -m QuizGenerator.regenerate --encrypted_str "EzE6JF86CDlf..." --points 5.0
21
+
16
22
  API Usage (recommended for web UIs):
17
23
  from grade_from_qr import regenerate_from_encrypted
18
24
 
@@ -36,9 +42,22 @@ import argparse
36
42
  import json
37
43
  import sys
38
44
  import logging
45
+ import os
39
46
  from pathlib import Path
40
47
  from typing import Dict, Any, Optional, List
41
48
 
49
+ # Load environment variables from .env file
50
+ try:
51
+ from dotenv import load_dotenv
52
+ # Try loading from current directory first, then home directory
53
+ if os.path.exists('.env'):
54
+ load_dotenv('.env')
55
+ else:
56
+ load_dotenv(os.path.join(os.path.expanduser("~"), ".env"))
57
+ except ImportError:
58
+ # dotenv not available, will use system environment variables only
59
+ pass
60
+
42
61
  # Quiz generator imports (always available)
43
62
  from QuizGenerator.qrcode_generator import QuestionQRCode
44
63
  from QuizGenerator.question import QuestionRegistry
@@ -400,6 +419,17 @@ def main():
400
419
  type=str,
401
420
  help='Path to image file containing QR code(s)'
402
421
  )
422
+ parser.add_argument(
423
+ '--encrypted_str',
424
+ type=str,
425
+ help='Encrypted string from QR code to decode directly'
426
+ )
427
+ parser.add_argument(
428
+ '--points',
429
+ type=float,
430
+ default=1.0,
431
+ help='Point value for the question (default: 1.0, only used with --encrypted_str)'
432
+ )
403
433
  parser.add_argument(
404
434
  '--all',
405
435
  action='store_true',
@@ -415,44 +445,83 @@ def main():
415
445
  action='store_true',
416
446
  help='Enable verbose debug logging'
417
447
  )
418
-
448
+
419
449
  args = parser.parse_args()
420
-
450
+
421
451
  if args.verbose:
422
452
  logging.getLogger().setLevel(logging.DEBUG)
423
-
424
- if not args.image:
453
+
454
+ # Check for required arguments
455
+ if not args.image and not args.encrypted_str:
425
456
  parser.print_help()
426
- print("\nERROR: --image is required")
457
+ print("\nERROR: Either --image or --encrypted_str is required")
427
458
  sys.exit(1)
428
-
429
- # Scan QR codes from image
430
- qr_codes = scan_qr_from_image(args.image)
431
-
432
- if not qr_codes:
433
- print("No QR codes found in image")
459
+
460
+ if args.image and args.encrypted_str:
461
+ print("ERROR: Cannot use both --image and --encrypted_str at the same time")
434
462
  sys.exit(1)
435
-
436
- # Process QR codes
437
- if not args.all:
438
- qr_codes = qr_codes[:1]
439
- log.info("Processing only the first QR code (use --all to process all)")
440
-
463
+
441
464
  results = []
442
-
443
- for qr_string in qr_codes:
444
- # Parse QR data
445
- qr_data = parse_qr_data(qr_string)
446
-
447
- if not qr_data:
448
- continue
449
-
450
- # Regenerate question and answer
451
- question_data = regenerate_question_answer(qr_data)
452
-
453
- if question_data:
465
+
466
+ # Handle direct encrypted string input
467
+ if args.encrypted_str:
468
+ try:
469
+ log.info(f"Decoding encrypted string (points={args.points})")
470
+ result = regenerate_from_encrypted(args.encrypted_str, args.points)
471
+
472
+ # Format result similar to regenerate_question_answer output
473
+ question_data = {
474
+ "question_number": "N/A",
475
+ "points": args.points,
476
+ "question_type": result["question_type"],
477
+ "seed": result["seed"],
478
+ "version": result["version"],
479
+ "answers": result["answers"],
480
+ "answer_objects": result["answer_objects"],
481
+ "answer_key_html": result["answer_key_html"],
482
+ "explanation_markdown": result.get("explanation_markdown")
483
+ }
484
+
485
+ if "kwargs" in result:
486
+ question_data["config"] = result["kwargs"]
487
+
454
488
  results.append(question_data)
455
489
  display_answer_summary(question_data)
490
+
491
+ except Exception as e:
492
+ log.error(f"Failed to decode encrypted string: {e}")
493
+ import traceback
494
+ if args.verbose:
495
+ log.debug(traceback.format_exc())
496
+ sys.exit(1)
497
+
498
+ # Handle image scanning
499
+ else:
500
+ # Scan QR codes from image
501
+ qr_codes = scan_qr_from_image(args.image)
502
+
503
+ if not qr_codes:
504
+ print("No QR codes found in image")
505
+ sys.exit(1)
506
+
507
+ # Process QR codes
508
+ if not args.all:
509
+ qr_codes = qr_codes[:1]
510
+ log.info("Processing only the first QR code (use --all to process all)")
511
+
512
+ for qr_string in qr_codes:
513
+ # Parse QR data
514
+ qr_data = parse_qr_data(qr_string)
515
+
516
+ if not qr_data:
517
+ continue
518
+
519
+ # Regenerate question and answer
520
+ question_data = regenerate_question_answer(qr_data)
521
+
522
+ if question_data:
523
+ results.append(question_data)
524
+ display_answer_summary(question_data)
456
525
 
457
526
  # Save to file if requested
458
527
  if args.output:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.4.2
3
+ Version: 0.6.0
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
@@ -1,52 +1,50 @@
1
- QuizGenerator/README.md,sha256=4n16gKyhIAKRBX4VKlpfcK0pyUYJ6Ht08MUsnwgxrZo,145
2
1
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
3
2
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
4
3
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
5
- QuizGenerator/contentast.py,sha256=nx0-y3LTyInzX9QheweHhA90zNsdtXie7X-ckYu1_rM,68318
6
- QuizGenerator/generate.py,sha256=o2XezoSE0u-qjxYu1_Ofm9Lpkza7M2Tg47C-ClMcPsE,7197
7
- QuizGenerator/logging.yaml,sha256=VJCdh26D8e_PNUs4McvvP1ojz9EVjQNifJzfhEk1Mbo,1114
8
- QuizGenerator/misc.py,sha256=2vEztj-Kt_0Q2OnynJKC4gL_w7l1MqWsBhhIDOuVD1s,18710
9
- QuizGenerator/mixins.py,sha256=zUKTkswq7aoDZ_nGPUdRuvnza8iH8ZCi6IH2Uw-kCvs,18492
4
+ QuizGenerator/contentast.py,sha256=lK92FlRfKnNU-9AhG8RJbCkpQixo5n7bmwvggyPazpM,93664
5
+ QuizGenerator/generate.py,sha256=bOkR6JhGQE0r4zbRWhC_3Dsj5hOEk6-jzFLcjk-qwKg,14811
6
+ QuizGenerator/misc.py,sha256=wtlrEpmEpoE6vNRmgjNUmuWnRdQKSCYfrqeoTagNaxg,464
7
+ QuizGenerator/mixins.py,sha256=HEhdGdeghqGWoajADTAIdUjkzwDSYl1b65LAkUdV50U,19211
10
8
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
11
9
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
12
- QuizGenerator/question.py,sha256=pyDQJzqqyEwhnqCl0PqdTNvM7bTPy7m31qdzGfYC0j4,28269
13
- QuizGenerator/quiz.py,sha256=toPodXea2UYGgAf4jyor3Gz-gtXYN1YUJFJFQ5u70v4,18718
14
- QuizGenerator/regenerate.py,sha256=EvtFhDUXYaWEBCGJ4RW-zN65qj3cMxWa_Y_Rn44WU6c,14282
10
+ QuizGenerator/question.py,sha256=FjiAJZn3LUBVr7nDazx5G0bASP0uXQTUP0rqphbvyLw,31714
11
+ QuizGenerator/quiz.py,sha256=f2HLrawUlu3ULkNDzcihBWAt-e-49AIPz_l1edMAEQ0,21503
12
+ QuizGenerator/regenerate.py,sha256=Uh4B9aKQvL3zD7PT-uH-GvrcSuUygV1BimvPVuErc-g,16525
15
13
  QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
16
14
  QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
17
15
  QuizGenerator/canvas/canvas_interface.py,sha256=wsEWh2lonUMgmbtXF-Zj59CAM_0NInoaERqsujlYMfc,24501
18
16
  QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
19
17
  QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- QuizGenerator/premade_questions/basic.py,sha256=wAvVZED6a7VToIvSCdAx6SrExmR0xVRo5dL40kycdXI,3402
18
+ QuizGenerator/premade_questions/basic.py,sha256=vMyCIYU0IJBjQVE-XVzHr9axq_kZL2ka4K1MaqeQwXM,3428
21
19
  QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
22
- QuizGenerator/premade_questions/cst334/languages.py,sha256=MTqprY8VUWgNlP0zRRpZXOAP2dd6ocx_XWVqcNlxYg8,14390
23
- QuizGenerator/premade_questions/cst334/math_questions.py,sha256=za8lNqhM0RB8qefmPP-Ww0WB_SQn0iRcBKOrZgyHCQQ,9290
24
- QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=B4hpnMliJY-x65hNbjwbf22m-jiTi3WEXmauKv_YA84,51598
20
+ QuizGenerator/premade_questions/cst334/languages.py,sha256=ENsgpup1uoGIVWEE_puDgLzWicguwdERsm-iiS-sh1E,15129
21
+ QuizGenerator/premade_questions/cst334/math_questions.py,sha256=zwkm3OLOkDZ_fbPF1E12UhdF373aQv9QzjBfV1bfbyU,10242
22
+ QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=PUyXIoyrGImXhucx6KBgoAEYmQCzTSCz0DWUu_xo6Kc,54371
25
23
  QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
26
- QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=hIOi-_K-0B_owWtV_YnXXW8Bb51uQF_lpVcXQkAlbXc,16520
27
- QuizGenerator/premade_questions/cst334/process.py,sha256=EB0iuT9Q8FfOnmlQoXL7gkfsPyVJP55cRFOe2mWfamc,23647
24
+ QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=pb63H47WlSsHi_nHRVhbwUHeybF2zbWL8vXbwOguAL0,17474
25
+ QuizGenerator/premade_questions/cst334/process.py,sha256=ERAdardtXSwzh3SDrlkVQJr4oO0F9cZtCZZ8dkJRsfw,39865
28
26
  QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
27
  QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
30
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=ssj6Xkpw6vDiL4qwVOiHUhly3TX50oX4KJtouj7qN6g,12809
31
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=VjmNDpV3YgeYKqyYp4wQXZIViZVvC1GcnjVUl7valhU,9724
32
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=TnhcD9OBsYgVDdSY5qPrSovnYB6r90wiGcg934czIrY,21500
33
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=iB3obG6-uXr_yrLVT7L_9j1H54f7oe5Rk9w45yW-lnw,2654
28
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=PdWAJjgsiwYQsxeLlQiVDd3m97RUjUY1KJTJxyrrdRI,13984
29
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=yc1wwqgsFh13peGDRJM74TaWmzQrFg-N7cyqIdf7G54,10874
30
+ QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=ftaNkwIPuh-wjAYeAj0YvklS3-xjOcQS7HvUkcNRRYY,22257
31
+ QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=sn70FLK_3LnhYhX7TVKB9oDwZMJybQIMtYLzxn5WZxg,2675
34
32
  QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
35
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=sq27xv7X9aa0axFxomusZRwM-ICj9grbhD_Bv3n3gJg,28947
36
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=tNxfR6J1cZHsHG9GfwVyl6lxxN_TEnhKDmMq4aVLwow,20793
33
+ QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=oU8Z36-P92TQ9OhJ2XK6ARlyM8I_qvp1guZLYVvU_l8,29116
34
+ QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=3NCgxtU0OxGrnixBOyrGKObf2rOWwXuBv4TWfiC9jxQ,13226
37
35
  QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- QuizGenerator/premade_questions/cst463/models/attention.py,sha256=i8h6DihzJTc_QFrdm1eaYhnuhlXKRUv_vIDg3jk_LZ8,5502
39
- QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=M0_9wlPhQICje1UdwIbDoBA4qzjmJtmP9VVVneYM5Mc,5766
36
+ QuizGenerator/premade_questions/cst463/models/attention.py,sha256=e_iPxGJUAXQ89zk8BXG9r1sGFKOJhBr4UaWaDQV6I7g,6100
37
+ QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=8Z3vP6A-CxRP80C7pb9gWJ2SctDh4X5fy6V5Gzx31Pc,6337
40
38
  QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
41
- QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=-tXeGgqPkctBBUy4RvEPqhv2kfPqoyO2wk-lNJLNWmY,6697
42
- QuizGenerator/premade_questions/cst463/models/text.py,sha256=bUiDIzOBEzilUKQjm2yO9ufcvJGY6Gt3qfeNP9UZOrc,6400
43
- QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=acygK-MobvdmwS4UYKVVL4Ey59M1qmq8dITWOT6V-aI,6793
39
+ QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=hHe-7npGmoUpYYVy1gIlyTTBk8ra3Muw0yHxa3FrbS8,7291
40
+ QuizGenerator/premade_questions/cst463/models/text.py,sha256=bwEebpYnHn52yCrzJGCNz8ebWIU03fdedI9JQmQ9I4I,6978
41
+ QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=oXyDKQvfLdWE_-cd2CY9m6t3SI_s-SQGXP6zDSoGINI,7273
44
42
  QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
45
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=gd5zI-6gmXUiK8GaCOCq0IlOwDg4YS0NlOdoN9v_flo,44810
43
+ QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=luWlTfj1UM1yQDQzs_tNzTV67qXhRUBwNt8QrV74XHs,46115
46
44
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
47
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=dPn8Sj0yk4m02np62esMKZ7CvcljhYq3Tq51nY9aJnA,29781
48
- quizgenerator-0.4.2.dist-info/METADATA,sha256=DDpI82uz-oqvKRYLPgF8O6-Cuv6g39So65_I82DBxFk,7212
49
- quizgenerator-0.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
- quizgenerator-0.4.2.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
51
- quizgenerator-0.4.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
52
- quizgenerator-0.4.2.dist-info/RECORD,,
45
+ QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=8Wo38kTd_n0Oau2ERpvcudB9uJiOVDYYQNeWu9v4Tyo,33516
46
+ quizgenerator-0.6.0.dist-info/METADATA,sha256=F6pWRrq1r55-tR5-lLyLpegiV26GxCeP4X7Y1w_NK8k,7212
47
+ quizgenerator-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ quizgenerator-0.6.0.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
+ quizgenerator-0.6.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
+ quizgenerator-0.6.0.dist-info/RECORD,,
QuizGenerator/README.md DELETED
@@ -1,5 +0,0 @@
1
-
2
-
3
- ## Installation
4
-
5
- Note, you will need to install `pandoc` prior to running since it is used to convert to HTML (for canvas) and Latex (for PDF)`