QuizGenerator 0.7.0__tar.gz → 0.7.1__tar.gz

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 (58) hide show
  1. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/PKG-INFO +1 -1
  2. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/regenerate.py +114 -13
  3. quizgenerator-0.7.0/pyproject_prev.toml → quizgenerator-0.7.1/pyproject.toml +1 -1
  4. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/uv.lock +1 -1
  5. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/.envrc +0 -0
  6. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/.gitignore +0 -0
  7. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/CODEOWNERS +0 -0
  8. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/LICENSE +0 -0
  9. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/__init__.py +0 -0
  10. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/__main__.py +0 -0
  11. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/canvas/__init__.py +0 -0
  12. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/canvas/canvas_interface.py +0 -0
  13. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/canvas/classes.py +0 -0
  14. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/constants.py +0 -0
  15. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/contentast.py +0 -0
  16. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/generate.py +0 -0
  17. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/misc.py +0 -0
  18. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/mixins.py +0 -0
  19. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/performance.py +0 -0
  20. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/__init__.py +0 -0
  21. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/basic.py +0 -0
  22. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/__init__.py +0 -0
  23. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/languages.py +0 -0
  24. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/math_questions.py +0 -0
  25. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/memory_questions.py +0 -0
  26. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +0 -0
  27. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/persistence_questions.py +0 -0
  28. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst334/process.py +0 -0
  29. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/__init__.py +0 -0
  30. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +0 -0
  31. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +0 -0
  32. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +0 -0
  33. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +0 -0
  34. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +0 -0
  35. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +0 -0
  36. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -0
  37. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -0
  38. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
  39. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/attention.py +0 -0
  40. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/cnns.py +0 -0
  41. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/matrices.py +0 -0
  42. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/rnns.py +0 -0
  43. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/text.py +0 -0
  44. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/models/weight_counting.py +0 -0
  45. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +0 -0
  46. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +0 -0
  47. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +0 -0
  48. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +0 -0
  49. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/qrcode_generator.py +0 -0
  50. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/question.py +0 -0
  51. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/quiz.py +0 -0
  52. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/QuizGenerator/typst_utils.py +0 -0
  53. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/README.md +0 -0
  54. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/examples/web_ui_integration_example.py +0 -0
  55. /quizgenerator-0.7.0/pyproject.toml → /quizgenerator-0.7.1/pyproject_prev.toml +0 -0
  56. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/scripts/generate_practice_yaml.sh +0 -0
  57. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/scripts/print.sh +0 -0
  58. {quizgenerator-0.7.0 → quizgenerator-0.7.1}/scripts/vendor_lms_interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.7.0
3
+ Version: 0.7.1
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
@@ -39,12 +39,13 @@ the exact question and answer without needing the original exam file.
39
39
  """
40
40
 
41
41
  import argparse
42
+ import base64
42
43
  import json
43
44
  import sys
44
45
  import logging
45
46
  import os
46
47
  from pathlib import Path
47
- from typing import Dict, Any, Optional, List
48
+ from typing import Dict, Any, Optional, List, Callable
48
49
 
49
50
  # Load environment variables from .env file
50
51
  try:
@@ -140,7 +141,39 @@ def parse_qr_data(qr_string: str) -> Dict[str, Any]:
140
141
  return {}
141
142
 
142
143
 
143
- def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
144
+ def _inline_image_upload(img_data) -> str:
145
+ img_data.seek(0)
146
+ b64 = base64.b64encode(img_data.read()).decode("ascii")
147
+ return f"data:image/png;base64,{b64}"
148
+
149
+
150
+ def _resolve_upload_func(
151
+ image_mode: str,
152
+ upload_func: Optional[Callable]
153
+ ) -> Optional[Callable]:
154
+ if image_mode == "inline":
155
+ return _inline_image_upload
156
+ if image_mode == "upload":
157
+ if upload_func is None:
158
+ raise ValueError("image_mode='upload' requires upload_func")
159
+ return upload_func
160
+ if image_mode == "none":
161
+ return None
162
+ raise ValueError(f"Unknown image_mode: {image_mode}")
163
+
164
+
165
+ def _render_html(element, upload_func=None, **kwargs) -> str:
166
+ if upload_func is None:
167
+ return element.render("html", **kwargs)
168
+ return element.render("html", upload_func=upload_func, **kwargs)
169
+
170
+
171
+ def regenerate_question_answer(
172
+ qr_data: Dict[str, Any],
173
+ *,
174
+ image_mode: str = "inline",
175
+ upload_func: Optional[Callable] = None
176
+ ) -> Optional[Dict[str, Any]]:
144
177
  """
145
178
  Regenerate question and extract answer using QR code metadata.
146
179
 
@@ -156,8 +189,9 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
156
189
  "seed": int,
157
190
  "version": str,
158
191
  "answers": dict,
159
- "explanation_markdown": str | None # Markdown explanation (None if not available)
160
- }
192
+ "explanation_markdown": str | None # Markdown explanation (None if not available)
193
+ "explanation_html": str | None # HTML explanation (None if not available)
194
+ }
161
195
  """
162
196
  question_num = qr_data.get('q')
163
197
  points = qr_data.get('pts')
@@ -220,8 +254,14 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
220
254
  # Also store the raw answer objects for easier access
221
255
  result['answer_objects'] = question.answers
222
256
 
257
+ resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
258
+
223
259
  # Generate HTML answer key for grading
224
- question_html = question_ast.body.render("html", show_answers=True)
260
+ question_html = _render_html(
261
+ question_ast.body,
262
+ show_answers=True,
263
+ upload_func=resolved_upload_func
264
+ )
225
265
  result['answer_key_html'] = question_html
226
266
 
227
267
  # Generate markdown explanation for students
@@ -232,6 +272,16 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
232
272
  else:
233
273
  result['explanation_markdown'] = explanation_markdown
234
274
 
275
+ # Generate HTML explanation (optional for web UIs)
276
+ explanation_html = _render_html(
277
+ question_ast.explanation,
278
+ upload_func=resolved_upload_func
279
+ )
280
+ if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
281
+ result["explanation_html"] = None
282
+ else:
283
+ result["explanation_html"] = explanation_html
284
+
235
285
  log.info(f" Successfully regenerated question with {len(canvas_answers)} answer(s)")
236
286
 
237
287
  return result
@@ -243,7 +293,13 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
243
293
  return result
244
294
 
245
295
 
246
- def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[str, Any]:
296
+ def regenerate_from_encrypted(
297
+ encrypted_data: str,
298
+ points: float = 1.0,
299
+ *,
300
+ image_mode: str = "inline",
301
+ upload_func: Optional[Callable] = None
302
+ ) -> Dict[str, Any]:
247
303
  """
248
304
  Regenerate question answers from encrypted QR code data (RECOMMENDED API).
249
305
 
@@ -253,6 +309,8 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
253
309
  Args:
254
310
  encrypted_data: The encrypted 's' field from the QR code JSON
255
311
  points: Point value for the question (default: 1.0)
312
+ image_mode: "inline", "upload", or "none" for HTML image handling
313
+ upload_func: Optional upload function used when image_mode="upload"
256
314
 
257
315
  Returns:
258
316
  Dictionary with regenerated answers:
@@ -266,6 +324,7 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
266
324
  "answer_objects": dict, # Raw Answer objects with values/tolerances
267
325
  "answer_key_html": str, # HTML rendering of question with answers shown
268
326
  "explanation_markdown": str | None # Markdown explanation (None if not available)
327
+ "explanation_html": str | None # HTML explanation (None if not available)
269
328
  }
270
329
 
271
330
  Raises:
@@ -287,12 +346,21 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
287
346
  kwargs = decrypted.get('config', {})
288
347
 
289
348
  # Use the existing regeneration logic
290
- return regenerate_from_metadata(question_type, seed, version, points, kwargs)
349
+ return regenerate_from_metadata(
350
+ question_type,
351
+ seed,
352
+ version,
353
+ points,
354
+ kwargs,
355
+ image_mode=image_mode,
356
+ upload_func=upload_func
357
+ )
291
358
 
292
359
 
293
360
  def regenerate_from_metadata(
294
361
  question_type: str, seed: int, version: str,
295
- points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None
362
+ points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None,
363
+ *, image_mode: str = "inline", upload_func: Optional[Callable] = None
296
364
  ) -> Dict[str, Any]:
297
365
  """
298
366
  Regenerate question answers from explicit metadata fields.
@@ -306,6 +374,8 @@ def regenerate_from_metadata(
306
374
  points: Point value for the question (default: 1.0)
307
375
  kwargs: Optional dictionary of question-specific configuration parameters
308
376
  (e.g., {"num_bits_va": 32, "max_value": 100})
377
+ image_mode: "inline", "upload", or "none" for HTML image handling
378
+ upload_func: Optional upload function used when image_mode="upload"
309
379
 
310
380
  Returns:
311
381
  Dictionary with regenerated answers (same format as regenerate_from_encrypted)
@@ -335,8 +405,14 @@ def regenerate_from_metadata(
335
405
  # Extract answers
336
406
  answer_kind, canvas_answers = question.get_answers()
337
407
 
408
+ resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
409
+
338
410
  # Generate HTML answer key for grading
339
- question_html = question_ast.body.render("html", show_answers=True)
411
+ question_html = _render_html(
412
+ question_ast.body,
413
+ show_answers=True,
414
+ upload_func=resolved_upload_func
415
+ )
340
416
 
341
417
  # Generate markdown explanation for students
342
418
  explanation_markdown = question_ast.explanation.render("markdown")
@@ -344,6 +420,13 @@ def regenerate_from_metadata(
344
420
  if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
345
421
  explanation_markdown = None
346
422
 
423
+ explanation_html = _render_html(
424
+ question_ast.explanation,
425
+ upload_func=resolved_upload_func
426
+ )
427
+ if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
428
+ explanation_html = None
429
+
347
430
  result = {
348
431
  "question_type": question_type,
349
432
  "seed": seed,
@@ -355,7 +438,8 @@ def regenerate_from_metadata(
355
438
  },
356
439
  "answer_objects": question.answers,
357
440
  "answer_key_html": question_html,
358
- "explanation_markdown": explanation_markdown
441
+ "explanation_markdown": explanation_markdown,
442
+ "explanation_html": explanation_html
359
443
  }
360
444
 
361
445
  # Include kwargs in result if provided
@@ -407,6 +491,9 @@ def display_answer_summary(question_data: Dict[str, Any]) -> None:
407
491
  if 'explanation_markdown' in question_data and question_data['explanation_markdown'] is not None:
408
492
  print("Markdown explanation available in result['explanation_markdown']")
409
493
 
494
+ if 'explanation_html' in question_data and question_data['explanation_html'] is not None:
495
+ print("HTML explanation available in result['explanation_html']")
496
+
410
497
  print("=" * 60)
411
498
 
412
499
 
@@ -445,6 +532,12 @@ def main():
445
532
  action='store_true',
446
533
  help='Enable verbose debug logging'
447
534
  )
535
+ parser.add_argument(
536
+ '--image-mode',
537
+ choices=['inline', 'none'],
538
+ default='inline',
539
+ help='HTML image handling (default: inline)'
540
+ )
448
541
 
449
542
  args = parser.parse_args()
450
543
 
@@ -467,7 +560,11 @@ def main():
467
560
  if args.encrypted_str:
468
561
  try:
469
562
  log.info(f"Decoding encrypted string (points={args.points})")
470
- result = regenerate_from_encrypted(args.encrypted_str, args.points)
563
+ result = regenerate_from_encrypted(
564
+ args.encrypted_str,
565
+ args.points,
566
+ image_mode=args.image_mode
567
+ )
471
568
 
472
569
  # Format result similar to regenerate_question_answer output
473
570
  question_data = {
@@ -479,7 +576,8 @@ def main():
479
576
  "answers": result["answers"],
480
577
  "answer_objects": result["answer_objects"],
481
578
  "answer_key_html": result["answer_key_html"],
482
- "explanation_markdown": result.get("explanation_markdown")
579
+ "explanation_markdown": result.get("explanation_markdown"),
580
+ "explanation_html": result.get("explanation_html")
483
581
  }
484
582
 
485
583
  if "kwargs" in result:
@@ -517,7 +615,10 @@ def main():
517
615
  continue
518
616
 
519
617
  # Regenerate question and answer
520
- question_data = regenerate_question_answer(qr_data)
618
+ question_data = regenerate_question_answer(
619
+ qr_data,
620
+ image_mode=args.image_mode
621
+ )
521
622
 
522
623
  if question_data:
523
624
  results.append(question_data)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "QuizGenerator"
7
- version = "0.6.3"
7
+ version = "0.7.1"
8
8
  description = "Generate randomized quiz questions for Canvas LMS and PDF exams"
9
9
  readme = "README.md"
10
10
  license = {text = "GPL-3.0-or-later"}
@@ -1214,7 +1214,7 @@ wheels = [
1214
1214
 
1215
1215
  [[package]]
1216
1216
  name = "quizgenerator"
1217
- version = "0.7.0"
1217
+ version = "0.7.1"
1218
1218
  source = { editable = "." }
1219
1219
  dependencies = [
1220
1220
  { name = "canvasapi" },
File without changes
File without changes
File without changes
File without changes
File without changes