QuizGenerator 0.5.0__tar.gz → 0.5.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.
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/PKG-INFO +1 -1
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/contentast.py +3 -2
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/generate.py +1 -2
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/process.py +503 -44
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/quiz.py +7 -1
- quizgenerator-0.5.1/pyproject.toml +76 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/uv.lock +1 -1
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/.envrc +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/.gitignore +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/CODEOWNERS +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/LICENSE +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/__main__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/canvas/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/canvas/canvas_interface.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/canvas/classes.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/constants.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/misc.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/mixins.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/performance.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/basic.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/languages.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/math_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/memory_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/persistence_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/attention.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/cnns.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/matrices.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/rnns.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/text.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/weight_counting.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/qrcode_generator.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/question.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/regenerate.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/typst_utils.py +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/README.md +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/examples/web_ui_integration_example.py +0 -0
- /quizgenerator-0.5.0/pyproject.toml → /quizgenerator-0.5.1/pyproject_prev.toml +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/scripts/generate_practice_yaml.sh +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.1}/scripts/print.sh +0 -0
- {quizgenerator-0.5.0 → quizgenerator-0.5.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.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -17,6 +17,9 @@ from QuizGenerator.qrcode_generator import QuestionQRCode
|
|
|
17
17
|
import re
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
+
import os
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
20
23
|
log = logging.getLogger(__name__)
|
|
21
24
|
|
|
22
25
|
class ContentAST:
|
|
@@ -1389,8 +1392,6 @@ class ContentAST:
|
|
|
1389
1392
|
def _ensure_image_saved(self):
|
|
1390
1393
|
"""Save image data to file if not already saved."""
|
|
1391
1394
|
if self.path is None:
|
|
1392
|
-
import os
|
|
1393
|
-
import uuid
|
|
1394
1395
|
|
|
1395
1396
|
# Create imgs directory if it doesn't exist (use absolute path)
|
|
1396
1397
|
img_dir = os.path.abspath("imgs")
|
|
@@ -42,8 +42,7 @@ def parse_args():
|
|
|
42
42
|
|
|
43
43
|
# PDF Flags
|
|
44
44
|
parser.add_argument("--num_pdfs", default=0, type=int, help="How many PDF quizzes to create")
|
|
45
|
-
parser.add_argument("--
|
|
46
|
-
help="Use Typst instead of LaTeX for PDF generation")
|
|
45
|
+
parser.add_argument("--latex", action="store_false", dest="typst", help="Use Typst instead of LaTeX for PDF generation")
|
|
47
46
|
|
|
48
47
|
subparsers = parser.add_subparsers(dest='command')
|
|
49
48
|
test_parser = subparsers.add_parser("TEST")
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/process.py
RENAMED
|
@@ -7,9 +7,7 @@ import dataclasses
|
|
|
7
7
|
import enum
|
|
8
8
|
import io
|
|
9
9
|
import logging
|
|
10
|
-
import math
|
|
11
10
|
import os
|
|
12
|
-
import queue
|
|
13
11
|
import uuid
|
|
14
12
|
from typing import List
|
|
15
13
|
|
|
@@ -65,6 +63,8 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
65
63
|
TIME_QUANTUM = None
|
|
66
64
|
|
|
67
65
|
ROUNDING_DIGITS = 2
|
|
66
|
+
IMAGE_DPI = 140
|
|
67
|
+
IMAGE_FIGSIZE = (9.5, 5.5)
|
|
68
68
|
|
|
69
69
|
@dataclasses.dataclass
|
|
70
70
|
class Job:
|
|
@@ -552,7 +552,7 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
552
552
|
|
|
553
553
|
def make_image(self):
|
|
554
554
|
|
|
555
|
-
fig, ax = plt.subplots(1, 1)
|
|
555
|
+
fig, ax = plt.subplots(1, 1, figsize=self.IMAGE_FIGSIZE, dpi=self.IMAGE_DPI)
|
|
556
556
|
|
|
557
557
|
for x_loc in set([t for job_id in self.job_stats.keys() for t in self.job_stats[job_id]["state_changes"] ]):
|
|
558
558
|
ax.axvline(x_loc, zorder=0)
|
|
@@ -610,7 +610,8 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
610
610
|
|
|
611
611
|
# Save to BytesIO object instead of a file
|
|
612
612
|
buffer = io.BytesIO()
|
|
613
|
-
plt.
|
|
613
|
+
plt.tight_layout()
|
|
614
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight', pad_inches=0.2)
|
|
614
615
|
plt.close(fig)
|
|
615
616
|
|
|
616
617
|
# Reset buffer position to the beginning
|
|
@@ -630,50 +631,508 @@ class SchedulingQuestion(ProcessQuestion, RegenerableChoiceMixin, TableQuestionM
|
|
|
630
631
|
return image_path
|
|
631
632
|
|
|
632
633
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
MIN_DURATION =
|
|
636
|
-
MAX_DURATION =
|
|
634
|
+
@QuestionRegistry.register()
|
|
635
|
+
class MLFQQuestion(ProcessQuestion, TableQuestionMixin, BodyTemplatesMixin):
|
|
636
|
+
MIN_DURATION = 4
|
|
637
|
+
MAX_DURATION = 12
|
|
637
638
|
MIN_ARRIVAL = 0
|
|
638
|
-
MAX_ARRIVAL =
|
|
639
|
-
|
|
639
|
+
MAX_ARRIVAL = 10
|
|
640
|
+
DEFAULT_NUM_JOBS = 3
|
|
641
|
+
DEFAULT_NUM_QUEUES = 3
|
|
642
|
+
ROUNDING_DIGITS = 2
|
|
643
|
+
IMAGE_DPI = 140
|
|
644
|
+
IMAGE_FIGSIZE = (9.5, 6.5)
|
|
645
|
+
|
|
640
646
|
@dataclasses.dataclass
|
|
641
|
-
class Job
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
647
|
+
class Job:
|
|
648
|
+
job_id: int
|
|
649
|
+
arrival_time: int
|
|
650
|
+
duration: int
|
|
651
|
+
remaining_time: int
|
|
652
|
+
queue_level: int = 0
|
|
653
|
+
time_in_queue: int = 0
|
|
654
|
+
response_time: float | None = None
|
|
655
|
+
turnaround_time: float | None = None
|
|
656
|
+
remaining_quantum: int | None = None
|
|
657
|
+
run_intervals: List[tuple] = dataclasses.field(default_factory=list)
|
|
658
|
+
max_queue_level: int = 0
|
|
659
|
+
|
|
660
|
+
def __init__(
|
|
661
|
+
self,
|
|
662
|
+
num_jobs: int = DEFAULT_NUM_JOBS,
|
|
663
|
+
num_queues: int = DEFAULT_NUM_QUEUES,
|
|
664
|
+
min_job_length: int = MIN_DURATION,
|
|
665
|
+
max_job_length: int = MAX_DURATION,
|
|
666
|
+
boost_interval: int | None = None,
|
|
667
|
+
boost_interval_range: List[int] | None = None,
|
|
668
|
+
*args,
|
|
669
|
+
**kwargs
|
|
670
|
+
):
|
|
671
|
+
kwargs["num_jobs"] = num_jobs
|
|
672
|
+
kwargs["num_queues"] = num_queues
|
|
673
|
+
kwargs["min_job_length"] = min_job_length
|
|
674
|
+
kwargs["max_job_length"] = max_job_length
|
|
675
|
+
if boost_interval is not None:
|
|
676
|
+
kwargs["boost_interval"] = boost_interval
|
|
677
|
+
if boost_interval_range is not None:
|
|
678
|
+
kwargs["boost_interval_range"] = boost_interval_range
|
|
679
|
+
super().__init__(*args, **kwargs)
|
|
680
|
+
self.num_jobs = num_jobs
|
|
681
|
+
self.num_queues = num_queues
|
|
682
|
+
self.min_job_length = min_job_length
|
|
683
|
+
self.max_job_length = max_job_length
|
|
684
|
+
self.boost_interval = boost_interval
|
|
685
|
+
self.boost_interval_range = boost_interval_range
|
|
686
|
+
|
|
687
|
+
def get_workload(self, num_jobs: int) -> List[MLFQQuestion.Job]:
|
|
688
|
+
arrivals = [0]
|
|
689
|
+
if num_jobs > 1:
|
|
690
|
+
arrivals.extend(
|
|
691
|
+
self.rng.randint(self.MIN_ARRIVAL, self.MAX_ARRIVAL)
|
|
692
|
+
for _ in range(num_jobs - 1)
|
|
693
|
+
)
|
|
694
|
+
if max(arrivals) == 0:
|
|
695
|
+
arrivals[-1] = self.rng.randint(1, self.MAX_ARRIVAL)
|
|
696
|
+
|
|
697
|
+
durations = [
|
|
698
|
+
self.rng.randint(self.min_job_length, self.max_job_length)
|
|
699
|
+
for _ in range(num_jobs)
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
jobs = []
|
|
703
|
+
for i in range(num_jobs):
|
|
704
|
+
jobs.append(
|
|
705
|
+
MLFQQuestion.Job(
|
|
706
|
+
job_id=i,
|
|
707
|
+
arrival_time=arrivals[i],
|
|
708
|
+
duration=durations[i],
|
|
709
|
+
remaining_time=durations[i],
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
return jobs
|
|
713
|
+
|
|
714
|
+
def _normalize_queue_params(self, values: List[int] | None, num_queues: int) -> List[int]:
|
|
715
|
+
if values is None:
|
|
716
|
+
return []
|
|
717
|
+
values = list(values)
|
|
718
|
+
while len(values) < num_queues:
|
|
719
|
+
values.append(values[-1])
|
|
720
|
+
return values[:num_queues]
|
|
721
|
+
|
|
722
|
+
def run_simulation(
|
|
723
|
+
self,
|
|
724
|
+
jobs: List[MLFQQuestion.Job],
|
|
725
|
+
queue_quantums: List[int],
|
|
726
|
+
queue_allotments: List[int | None],
|
|
727
|
+
boost_interval: int | None,
|
|
728
|
+
) -> None:
|
|
729
|
+
self.timeline = collections.defaultdict(list)
|
|
730
|
+
self.boost_times = []
|
|
731
|
+
pending = sorted(jobs, key=lambda j: (j.arrival_time, j.job_id))
|
|
732
|
+
queues = [collections.deque() for _ in range(len(queue_quantums))]
|
|
733
|
+
completed = set()
|
|
734
|
+
|
|
735
|
+
curr_time = pending[0].arrival_time if pending else 0
|
|
736
|
+
self.timeline[curr_time].append("Simulation Start")
|
|
737
|
+
next_boost_time = None
|
|
738
|
+
if boost_interval is not None:
|
|
739
|
+
next_boost_time = boost_interval
|
|
740
|
+
|
|
741
|
+
def enqueue_arrivals(up_to_time: int) -> None:
|
|
742
|
+
nonlocal pending
|
|
743
|
+
while pending and pending[0].arrival_time <= up_to_time:
|
|
744
|
+
job = pending.pop(0)
|
|
745
|
+
job.queue_level = len(queues) - 1
|
|
746
|
+
job.time_in_queue = 0
|
|
747
|
+
job.remaining_quantum = None
|
|
748
|
+
queues[-1].append(job)
|
|
749
|
+
self.timeline[job.arrival_time].append(
|
|
750
|
+
f"Job{job.job_id} arrived (dur = {job.duration})"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
def apply_boost(curr_time: int, running_job: MLFQQuestion.Job | None = None) -> None:
|
|
754
|
+
jobs_to_boost = []
|
|
755
|
+
for q in queues:
|
|
756
|
+
while q:
|
|
757
|
+
jobs_to_boost.append(q.popleft())
|
|
758
|
+
if running_job is not None and running_job.remaining_time > 0:
|
|
759
|
+
jobs_to_boost.append(running_job)
|
|
760
|
+
if not jobs_to_boost:
|
|
761
|
+
self.boost_times.append(curr_time)
|
|
762
|
+
return
|
|
763
|
+
for job in sorted(jobs_to_boost, key=lambda j: j.job_id):
|
|
764
|
+
job.queue_level = len(queues) - 1
|
|
765
|
+
job.time_in_queue = 0
|
|
766
|
+
job.remaining_quantum = None
|
|
767
|
+
queues[-1].append(job)
|
|
768
|
+
self.timeline[curr_time].append(
|
|
769
|
+
f"Boosted all jobs to Q{len(queues) - 1}"
|
|
770
|
+
)
|
|
771
|
+
self.boost_times.append(curr_time)
|
|
772
|
+
|
|
773
|
+
enqueue_arrivals(curr_time)
|
|
774
|
+
|
|
775
|
+
while len(completed) < len(jobs):
|
|
776
|
+
q_idx = next(
|
|
777
|
+
(i for i in range(len(queues) - 1, -1, -1) if queues[i]),
|
|
778
|
+
None
|
|
779
|
+
)
|
|
780
|
+
if q_idx is None:
|
|
781
|
+
next_times = []
|
|
782
|
+
if pending:
|
|
783
|
+
next_times.append(pending[0].arrival_time)
|
|
784
|
+
if next_boost_time is not None:
|
|
785
|
+
next_times.append(next_boost_time)
|
|
786
|
+
if next_times:
|
|
787
|
+
next_time = min(next_times)
|
|
788
|
+
if next_time > curr_time:
|
|
789
|
+
self.timeline[curr_time].append("CPU idle")
|
|
790
|
+
curr_time = next_time
|
|
791
|
+
enqueue_arrivals(curr_time)
|
|
792
|
+
while next_boost_time is not None and curr_time >= next_boost_time:
|
|
793
|
+
apply_boost(curr_time)
|
|
794
|
+
next_boost_time += boost_interval
|
|
795
|
+
continue
|
|
796
|
+
break
|
|
797
|
+
|
|
798
|
+
job = queues[q_idx].popleft()
|
|
799
|
+
quantum = queue_quantums[q_idx]
|
|
800
|
+
if job.remaining_quantum is None or job.remaining_quantum <= 0:
|
|
801
|
+
job.remaining_quantum = quantum
|
|
802
|
+
|
|
803
|
+
slice_duration = min(job.remaining_quantum, job.remaining_time)
|
|
804
|
+
preempted = False
|
|
805
|
+
if q_idx < len(queues) - 1 and pending:
|
|
806
|
+
next_arrival = pending[0].arrival_time
|
|
807
|
+
if next_arrival < curr_time + slice_duration:
|
|
808
|
+
slice_duration = next_arrival - curr_time
|
|
809
|
+
preempted = True
|
|
810
|
+
if next_boost_time is not None and next_boost_time < curr_time + slice_duration:
|
|
811
|
+
slice_duration = next_boost_time - curr_time
|
|
812
|
+
preempted = True
|
|
813
|
+
|
|
814
|
+
if job.response_time is None:
|
|
815
|
+
job.response_time = curr_time - job.arrival_time
|
|
816
|
+
|
|
817
|
+
if slice_duration > 0:
|
|
818
|
+
self.timeline[curr_time].append(
|
|
819
|
+
f"Running Job{job.job_id} in Q{q_idx} for {slice_duration}"
|
|
820
|
+
)
|
|
821
|
+
job.run_intervals.append((curr_time, curr_time + slice_duration, q_idx))
|
|
822
|
+
curr_time += slice_duration
|
|
823
|
+
job.remaining_time -= slice_duration
|
|
824
|
+
job.time_in_queue += slice_duration
|
|
825
|
+
job.remaining_quantum -= slice_duration
|
|
826
|
+
|
|
827
|
+
enqueue_arrivals(curr_time)
|
|
828
|
+
boosted_current_job = False
|
|
829
|
+
while next_boost_time is not None and curr_time >= next_boost_time:
|
|
830
|
+
apply_boost(curr_time, running_job=job if not boosted_current_job else None)
|
|
831
|
+
boosted_current_job = True
|
|
832
|
+
next_boost_time += boost_interval
|
|
833
|
+
|
|
834
|
+
if job.remaining_time <= 0:
|
|
835
|
+
job.turnaround_time = curr_time - job.arrival_time
|
|
836
|
+
job.remaining_quantum = None
|
|
837
|
+
completed.add(job.job_id)
|
|
838
|
+
self.timeline[curr_time].append(
|
|
839
|
+
f"Completed Job{job.job_id} (TAT = {job.turnaround_time})"
|
|
840
|
+
)
|
|
841
|
+
continue
|
|
842
|
+
if boosted_current_job:
|
|
843
|
+
continue
|
|
844
|
+
|
|
845
|
+
allotment = queue_allotments[q_idx]
|
|
846
|
+
if (
|
|
847
|
+
allotment is not None
|
|
848
|
+
and job.time_in_queue >= allotment
|
|
849
|
+
and q_idx > 0
|
|
850
|
+
):
|
|
851
|
+
job.queue_level = q_idx - 1
|
|
852
|
+
job.max_queue_level = max(job.max_queue_level, job.queue_level)
|
|
853
|
+
job.time_in_queue = 0
|
|
854
|
+
job.remaining_quantum = None
|
|
855
|
+
queues[q_idx - 1].append(job)
|
|
856
|
+
self.timeline[curr_time].append(
|
|
857
|
+
f"Demoted Job{job.job_id} to Q{q_idx - 1}"
|
|
858
|
+
)
|
|
859
|
+
continue
|
|
860
|
+
|
|
861
|
+
if preempted and job.remaining_quantum > 0:
|
|
862
|
+
queues[q_idx].appendleft(job)
|
|
863
|
+
else:
|
|
864
|
+
if job.remaining_quantum <= 0:
|
|
865
|
+
job.remaining_quantum = None
|
|
866
|
+
queues[q_idx].append(job)
|
|
867
|
+
|
|
654
868
|
def refresh(self, *args, **kwargs):
|
|
655
869
|
super().refresh(*args, **kwargs)
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
870
|
+
|
|
871
|
+
self.num_jobs = kwargs.get("num_jobs", self.num_jobs)
|
|
872
|
+
self.num_queues = kwargs.get("num_queues", self.num_queues)
|
|
873
|
+
self.min_job_length = kwargs.get("min_job_length", self.min_job_length)
|
|
874
|
+
self.max_job_length = kwargs.get("max_job_length", self.max_job_length)
|
|
875
|
+
self.boost_interval = kwargs.get("boost_interval", self.boost_interval)
|
|
876
|
+
self.boost_interval_range = kwargs.get(
|
|
877
|
+
"boost_interval_range",
|
|
878
|
+
self.boost_interval_range
|
|
879
|
+
)
|
|
880
|
+
if self.boost_interval is None and self.boost_interval_range:
|
|
881
|
+
low, high = self.boost_interval_range
|
|
882
|
+
self.boost_interval = self.rng.randint(low, high)
|
|
883
|
+
|
|
884
|
+
jobs = self.get_workload(self.num_jobs)
|
|
885
|
+
|
|
886
|
+
queue_quantums = [2**(self.num_queues - 1 - i) for i in range(self.num_queues)]
|
|
887
|
+
queue_quantums = self._normalize_queue_params(queue_quantums, self.num_queues)
|
|
888
|
+
queue_quantums = [int(q) for q in queue_quantums]
|
|
889
|
+
|
|
890
|
+
queue_allotments = [None] + [
|
|
891
|
+
queue_quantums[i] * 2 for i in range(1, self.num_queues)
|
|
892
|
+
]
|
|
893
|
+
queue_allotments = self._normalize_queue_params(queue_allotments, self.num_queues)
|
|
894
|
+
queue_allotments = [
|
|
895
|
+
int(allotment) if allotment is not None else None
|
|
896
|
+
for allotment in queue_allotments
|
|
897
|
+
]
|
|
898
|
+
queue_allotments[0] = None
|
|
899
|
+
|
|
900
|
+
self.queue_quantums = queue_quantums
|
|
901
|
+
self.queue_allotments = queue_allotments
|
|
902
|
+
|
|
903
|
+
self.run_simulation(jobs, queue_quantums, queue_allotments, self.boost_interval)
|
|
904
|
+
|
|
905
|
+
self.job_stats = {
|
|
906
|
+
job.job_id: {
|
|
907
|
+
"arrival_time": job.arrival_time,
|
|
908
|
+
"duration": job.duration,
|
|
909
|
+
"Response": job.response_time,
|
|
910
|
+
"TAT": job.turnaround_time,
|
|
911
|
+
"run_intervals": list(job.run_intervals),
|
|
912
|
+
}
|
|
913
|
+
for job in jobs
|
|
666
914
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
915
|
+
|
|
916
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
917
|
+
self.answers.update({
|
|
918
|
+
f"answer__turnaround_time_job{job_id}": Answer.auto_float(
|
|
919
|
+
f"answer__turnaround_time_job{job_id}",
|
|
920
|
+
self.job_stats[job_id]["TAT"]
|
|
921
|
+
)
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
return self.is_interesting()
|
|
925
|
+
|
|
926
|
+
def _get_body(self, *args, **kwargs):
|
|
927
|
+
answers: List[Answer] = []
|
|
928
|
+
|
|
929
|
+
queue_rows = []
|
|
930
|
+
for i in reversed(range(self.num_queues)):
|
|
931
|
+
allotment = self.queue_allotments[i]
|
|
932
|
+
queue_rows.append([
|
|
933
|
+
f"Q{i}",
|
|
934
|
+
self.queue_quantums[i],
|
|
935
|
+
"infinite" if allotment is None else allotment
|
|
936
|
+
])
|
|
937
|
+
queue_table = ContentAST.Table(
|
|
938
|
+
headers=["Queue", "Quantum", "Allotment"],
|
|
939
|
+
data=queue_rows
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
table_rows = []
|
|
943
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
944
|
+
table_rows.append({
|
|
945
|
+
"Job ID": f"Job{job_id}",
|
|
946
|
+
"Arrival": self.job_stats[job_id]["arrival_time"],
|
|
947
|
+
"Duration": self.job_stats[job_id]["duration"],
|
|
948
|
+
"TAT": f"answer__turnaround_time_job{job_id}",
|
|
949
|
+
})
|
|
950
|
+
answers.append(self.answers[f"answer__turnaround_time_job{job_id}"])
|
|
951
|
+
|
|
952
|
+
scheduling_table = self.create_answer_table(
|
|
953
|
+
headers=["Job ID", "Arrival", "Duration", "TAT"],
|
|
954
|
+
data_rows=table_rows,
|
|
955
|
+
answer_columns=["TAT"]
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
intro_text = (
|
|
959
|
+
"Assume an MLFQ scheduler with round-robin inside each queue. "
|
|
960
|
+
f"New jobs enter the highest-priority queue (Q{self.num_queues - 1}) "
|
|
961
|
+
"and a job is demoted after using its total allotment for that queue. "
|
|
962
|
+
"If a higher-priority job arrives, it preempts any lower-priority job."
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
instructions = (
|
|
966
|
+
f"Compute the turnaround time (TAT) for each job. "
|
|
967
|
+
f"Round to at most {Answer.DEFAULT_ROUNDING_DIGITS} digits after the decimal."
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
body = ContentAST.Section()
|
|
971
|
+
body.add_element(ContentAST.Paragraph([intro_text]))
|
|
972
|
+
body.add_element(queue_table)
|
|
973
|
+
if self.boost_interval is not None:
|
|
974
|
+
body.add_element(ContentAST.Paragraph([
|
|
975
|
+
f"Every {self.boost_interval} time units, all jobs are boosted to "
|
|
976
|
+
f"Q{self.num_queues - 1}. After a boost, scheduling restarts with the "
|
|
977
|
+
"lowest job number in that queue."
|
|
978
|
+
]))
|
|
979
|
+
body.add_element(ContentAST.Paragraph([instructions]))
|
|
980
|
+
body.add_element(scheduling_table)
|
|
981
|
+
return body, answers
|
|
982
|
+
|
|
983
|
+
def get_body(self, *args, **kwargs) -> ContentAST.Section:
|
|
984
|
+
body, _ = self._get_body(*args, **kwargs)
|
|
985
|
+
return body
|
|
986
|
+
|
|
987
|
+
def _get_explanation(self, **kwargs):
|
|
988
|
+
explanation = ContentAST.Section()
|
|
989
|
+
|
|
990
|
+
explanation.add_element(
|
|
991
|
+
ContentAST.Paragraph([
|
|
992
|
+
"Turnaround time (TAT) is the completion time minus the arrival time.",
|
|
993
|
+
"We calculate it for each job after simulating the schedule."
|
|
994
|
+
])
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
explanation.add_element(
|
|
998
|
+
ContentAST.Paragraph([
|
|
999
|
+
"For each job:"
|
|
1000
|
+
] + [
|
|
1001
|
+
f"Job{job_id}_TAT = "
|
|
1002
|
+
f"{self.job_stats[job_id]['arrival_time'] + self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f} "
|
|
1003
|
+
f"- {self.job_stats[job_id]['arrival_time']:0.{self.ROUNDING_DIGITS}f} "
|
|
1004
|
+
f"= {self.job_stats[job_id]['TAT']:0.{self.ROUNDING_DIGITS}f}"
|
|
1005
|
+
for job_id in sorted(self.job_stats.keys())
|
|
1006
|
+
])
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
explanation.add_element(
|
|
1010
|
+
ContentAST.Table(
|
|
1011
|
+
headers=["Time", "Events"],
|
|
1012
|
+
data=[
|
|
1013
|
+
[f"{t:0.{self.ROUNDING_DIGITS}f}s"] + ['\n'.join(events)]
|
|
1014
|
+
for t in sorted(self.timeline.keys())
|
|
1015
|
+
if (events := [
|
|
1016
|
+
event for event in self.timeline[t]
|
|
1017
|
+
if (
|
|
1018
|
+
"arrived" in event
|
|
1019
|
+
or "Demoted" in event
|
|
1020
|
+
or "Boosted" in event
|
|
1021
|
+
or "Completed" in event
|
|
1022
|
+
or "Simulation Start" in event
|
|
1023
|
+
or "CPU idle" in event
|
|
1024
|
+
)
|
|
1025
|
+
])
|
|
1026
|
+
]
|
|
1027
|
+
)
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
explanation.add_element(
|
|
1031
|
+
ContentAST.Picture(
|
|
1032
|
+
img_data=self.make_image(),
|
|
1033
|
+
caption="MLFQ Scheduling Overview"
|
|
1034
|
+
)
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
return explanation, []
|
|
1038
|
+
|
|
1039
|
+
def get_explanation(self, **kwargs) -> ContentAST.Section:
|
|
1040
|
+
explanation, _ = self._get_explanation(**kwargs)
|
|
1041
|
+
return explanation
|
|
1042
|
+
|
|
1043
|
+
def make_image(self):
|
|
1044
|
+
fig, ax = plt.subplots(1, 1, figsize=self.IMAGE_FIGSIZE, dpi=self.IMAGE_DPI)
|
|
1045
|
+
|
|
1046
|
+
num_jobs = len(self.job_stats)
|
|
1047
|
+
if num_jobs == 0:
|
|
1048
|
+
buffer = io.BytesIO()
|
|
1049
|
+
plt.tight_layout()
|
|
1050
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight')
|
|
1051
|
+
plt.close(fig)
|
|
1052
|
+
buffer.seek(0)
|
|
1053
|
+
return buffer
|
|
1054
|
+
|
|
1055
|
+
job_colors = {
|
|
1056
|
+
job_id: str(0.15 + 0.7 * (idx / max(1, num_jobs - 1)))
|
|
1057
|
+
for idx, job_id in enumerate(sorted(self.job_stats.keys()))
|
|
1058
|
+
}
|
|
1059
|
+
job_lane = {
|
|
1060
|
+
job_id: idx
|
|
1061
|
+
for idx, job_id in enumerate(sorted(self.job_stats.keys(), reverse=True))
|
|
1062
|
+
}
|
|
1063
|
+
lanes_per_queue = num_jobs
|
|
1064
|
+
|
|
1065
|
+
for job_id in sorted(self.job_stats.keys()):
|
|
1066
|
+
for start, stop, queue_level in self.job_stats[job_id]["run_intervals"]:
|
|
1067
|
+
y_loc = queue_level * lanes_per_queue + job_lane[job_id]
|
|
1068
|
+
ax.barh(
|
|
1069
|
+
y=[y_loc],
|
|
1070
|
+
left=[start],
|
|
1071
|
+
width=[stop - start],
|
|
1072
|
+
edgecolor='black',
|
|
1073
|
+
linewidth=1.5,
|
|
1074
|
+
color=job_colors[job_id]
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
for queue_idx in range(self.num_queues):
|
|
1078
|
+
if queue_idx % 2 == 1:
|
|
1079
|
+
ax.axhspan(
|
|
1080
|
+
queue_idx * lanes_per_queue - 0.5,
|
|
1081
|
+
(queue_idx + 1) * lanes_per_queue - 0.5,
|
|
1082
|
+
facecolor='0.97',
|
|
1083
|
+
edgecolor='none',
|
|
1084
|
+
zorder=-1
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
arrival_times = sorted({
|
|
1088
|
+
self.job_stats[job_id]["arrival_time"]
|
|
1089
|
+
for job_id in self.job_stats.keys()
|
|
1090
|
+
})
|
|
1091
|
+
bottom_label_y = -0.1
|
|
1092
|
+
for arrival_time in arrival_times:
|
|
1093
|
+
ax.axvline(arrival_time, color='0.2', linestyle=':', linewidth=1.2, zorder=0)
|
|
1094
|
+
ax.text(
|
|
1095
|
+
arrival_time + 0.2,
|
|
1096
|
+
bottom_label_y,
|
|
1097
|
+
f"{arrival_time:0.{self.ROUNDING_DIGITS}f}s",
|
|
1098
|
+
color='0.2',
|
|
1099
|
+
rotation=90,
|
|
1100
|
+
ha='left',
|
|
1101
|
+
va='bottom'
|
|
674
1102
|
)
|
|
1103
|
+
|
|
1104
|
+
completion_times = sorted({
|
|
1105
|
+
self.job_stats[job_id]["arrival_time"] + self.job_stats[job_id]["TAT"]
|
|
1106
|
+
for job_id in self.job_stats.keys()
|
|
1107
|
+
})
|
|
1108
|
+
for completion_time in completion_times:
|
|
1109
|
+
ax.axvline(completion_time, color='red', linewidth=1.5, zorder=0)
|
|
1110
|
+
ax.text(
|
|
1111
|
+
completion_time - 0.6,
|
|
1112
|
+
self.num_queues * lanes_per_queue - 0.5,
|
|
1113
|
+
f"{completion_time:0.{self.ROUNDING_DIGITS}f}s",
|
|
1114
|
+
color='red',
|
|
1115
|
+
rotation=90,
|
|
1116
|
+
ha='center',
|
|
1117
|
+
va='top'
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
for boost_time in sorted(set(self.boost_times)):
|
|
1121
|
+
ax.axvline(boost_time, color='tab:blue', linestyle='--', linewidth=1.2, zorder=0)
|
|
1122
|
+
|
|
1123
|
+
tick_positions = [
|
|
1124
|
+
q * lanes_per_queue + (lanes_per_queue - 1) / 2
|
|
1125
|
+
for q in range(self.num_queues)
|
|
675
1126
|
]
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1127
|
+
ax.set_yticks(tick_positions)
|
|
1128
|
+
ax.set_yticklabels([f"Q{i}" for i in range(self.num_queues)])
|
|
1129
|
+
ax.set_ylim(-0.5, self.num_queues * lanes_per_queue - 0.5)
|
|
1130
|
+
ax.set_xlim(xmin=0)
|
|
1131
|
+
ax.set_xlabel("Time")
|
|
1132
|
+
|
|
1133
|
+
buffer = io.BytesIO()
|
|
1134
|
+
plt.tight_layout()
|
|
1135
|
+
plt.savefig(buffer, format='png', dpi=self.IMAGE_DPI, bbox_inches='tight')
|
|
1136
|
+
plt.close(fig)
|
|
1137
|
+
buffer.seek(0)
|
|
1138
|
+
return buffer
|
|
@@ -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", [])))
|
|
@@ -515,4 +521,4 @@ def main():
|
|
|
515
521
|
|
|
516
522
|
if __name__ == "__main__":
|
|
517
523
|
main()
|
|
518
|
-
|
|
524
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "QuizGenerator"
|
|
7
|
+
version = "0.5.1"
|
|
8
|
+
description = "Generate randomized quiz questions for Canvas LMS and PDF exams"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "GPL-3.0-or-later"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Sam Ogden", email = "samuel.s.ogden@gmail.com"},
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
|
|
16
|
+
keywords = ["education", "quiz", "canvas", "lms", "assessment", "teaching", "exam", "testing"]
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Education",
|
|
21
|
+
"Topic :: Education :: Testing",
|
|
22
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"Jinja2==3.1.3",
|
|
29
|
+
"python-dotenv==1.0.1",
|
|
30
|
+
"PyYAML==6.0.1",
|
|
31
|
+
"requests==2.32.2",
|
|
32
|
+
"pypandoc~=1.6.3",
|
|
33
|
+
"pytablewriter~=1.2.0",
|
|
34
|
+
"pylatex>=1.4.2",
|
|
35
|
+
"matplotlib",
|
|
36
|
+
"sympy>=1.14.0",
|
|
37
|
+
"markdown>=3.9",
|
|
38
|
+
"segno>=1.6.0",
|
|
39
|
+
"cryptography>=41.0.0",
|
|
40
|
+
"graphviz>=0.21",
|
|
41
|
+
"canvasapi==3.2.0",
|
|
42
|
+
"keras>=3.12.0",
|
|
43
|
+
"tensorflow>=2.20.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/OtterDen-Lab/QuizGenerator"
|
|
48
|
+
Documentation = "https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation"
|
|
49
|
+
Repository = "https://github.com/OtterDen-Lab/QuizGenerator"
|
|
50
|
+
"Bug Tracker" = "https://github.com/OtterDen-Lab/QuizGenerator/issues"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
quizgen = "QuizGenerator.generate:main"
|
|
54
|
+
quizregen = "QuizGenerator.regenerate:main"
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
grading = [
|
|
58
|
+
"pyzbar>=0.1.9",
|
|
59
|
+
"pillow>=10.0.0",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.hatch.build.targets.wheel]
|
|
63
|
+
packages = ["QuizGenerator"]
|
|
64
|
+
|
|
65
|
+
[tool.hatch.metadata]
|
|
66
|
+
allow-direct-references = true
|
|
67
|
+
|
|
68
|
+
# Entry point group for custom question types (for plugin developers)
|
|
69
|
+
# Users can register custom question types in their own packages using:
|
|
70
|
+
# [project.entry-points."quizgenerator.questions"]
|
|
71
|
+
# my_custom_question = "my_package.module:MyQuestionClass"
|
|
72
|
+
#
|
|
73
|
+
# Example:
|
|
74
|
+
# [project.entry-points."quizgenerator.questions"]
|
|
75
|
+
# advanced_scheduling = "university_questions.os:AdvancedSchedulingQuestion"
|
|
76
|
+
# memory_hierarchy = "university_questions.os:MemoryHierarchyQuestion"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/__init__.py
RENAMED
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/languages.py
RENAMED
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/math_questions.py
RENAMED
|
File without changes
|
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst334/ostep13_vsfs.py
RENAMED
|
File without changes
|
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/cnns.py
RENAMED
|
File without changes
|
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/rnns.py
RENAMED
|
File without changes
|
{quizgenerator-0.5.0 → quizgenerator-0.5.1}/QuizGenerator/premade_questions/cst463/models/text.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|