randex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- randex/__init__.py +8 -0
- randex/exam.py +908 -0
- randex-0.1.0.dist-info/LICENSE +407 -0
- randex-0.1.0.dist-info/METADATA +190 -0
- randex-0.1.0.dist-info/RECORD +10 -0
- randex-0.1.0.dist-info/WHEEL +4 -0
- randex-0.1.0.dist-info/entry_points.txt +4 -0
- scripts/__init__.py +1 -0
- scripts/exams.py +136 -0
- scripts/validate.py +105 -0
scripts/exams.py
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
"""Script that creates a batch of randomized exams."""
|
2
|
+
|
3
|
+
import shutil
|
4
|
+
from datetime import datetime
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from randex.exam import ExamBatch, ExamTemplate, Pool, QuestionSet
|
10
|
+
|
11
|
+
|
12
|
+
@click.command(
|
13
|
+
context_settings={"help_option_names": ["--help"]},
|
14
|
+
)
|
15
|
+
@click.argument(
|
16
|
+
"folder",
|
17
|
+
type=str,
|
18
|
+
nargs=1,
|
19
|
+
required=True,
|
20
|
+
)
|
21
|
+
@click.option(
|
22
|
+
"--batch-size",
|
23
|
+
"-b",
|
24
|
+
type=int,
|
25
|
+
default=1,
|
26
|
+
help="Number of exams to be created",
|
27
|
+
)
|
28
|
+
@click.option(
|
29
|
+
"--number_of_questions",
|
30
|
+
"-n",
|
31
|
+
type=int,
|
32
|
+
default=[1],
|
33
|
+
multiple=True,
|
34
|
+
help="""
|
35
|
+
Specify how many questions to sample.
|
36
|
+
|
37
|
+
- Use once: sample total number of questions from all folders.
|
38
|
+
Example: -n 10
|
39
|
+
|
40
|
+
- Use multiple times: sample per-folder counts, in order.
|
41
|
+
Example: -n 5 -n 3 # 5 from folder 1, 3 from folder 2
|
42
|
+
""",
|
43
|
+
)
|
44
|
+
@click.option(
|
45
|
+
"--template-tex-path",
|
46
|
+
"-t",
|
47
|
+
type=click.Path(
|
48
|
+
exists=True,
|
49
|
+
resolve_path=True,
|
50
|
+
file_okay=True,
|
51
|
+
dir_okay=False,
|
52
|
+
path_type=Path,
|
53
|
+
),
|
54
|
+
required=True,
|
55
|
+
help="Path to the YAML file that contains the template for the exam configuration",
|
56
|
+
)
|
57
|
+
@click.option(
|
58
|
+
"--out-folder",
|
59
|
+
"-o",
|
60
|
+
type=Path,
|
61
|
+
help="Create the batch exams in this folder (default: tmp_HH-MM-SS)",
|
62
|
+
)
|
63
|
+
@click.option(
|
64
|
+
"--clean",
|
65
|
+
"-c",
|
66
|
+
is_flag=True,
|
67
|
+
default=False,
|
68
|
+
help="Clean all latex compilation auxiliary files",
|
69
|
+
)
|
70
|
+
@click.option(
|
71
|
+
"--overwrite",
|
72
|
+
is_flag=True,
|
73
|
+
default=False,
|
74
|
+
help="Overwrite the out-folder if it already exists (use with caution).",
|
75
|
+
)
|
76
|
+
def main(
|
77
|
+
folder: str,
|
78
|
+
number_of_questions: list | int,
|
79
|
+
batch_size: int,
|
80
|
+
template_tex_path: Path,
|
81
|
+
out_folder: Path | None,
|
82
|
+
clean: bool,
|
83
|
+
overwrite: bool,
|
84
|
+
) -> None:
|
85
|
+
"""
|
86
|
+
Create a batch of exams with randomly chosen multiple choice questions.
|
87
|
+
|
88
|
+
The questions are loaded from a list of FOLDERS.
|
89
|
+
|
90
|
+
FOLDER: Path or quoted glob (e.g. "data/unit_*").
|
91
|
+
|
92
|
+
The questions are loaded from the FOLDERs and must follow the format:
|
93
|
+
|
94
|
+
question: What is $1+1$?
|
95
|
+
answers: ["0", "1", "2", "3"]
|
96
|
+
right_answer: 2
|
97
|
+
|
98
|
+
💡 Remember to wrap glob patterns in quotes to prevent shell expansion!
|
99
|
+
"""
|
100
|
+
if out_folder is None:
|
101
|
+
out_folder = Path(f"tmp_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}")
|
102
|
+
|
103
|
+
if out_folder.exists():
|
104
|
+
if not overwrite:
|
105
|
+
raise click.UsageError(
|
106
|
+
f"Output folder '{out_folder}' already exists.\n"
|
107
|
+
"Use --overwrite to remove it and continue.",
|
108
|
+
)
|
109
|
+
shutil.rmtree(out_folder)
|
110
|
+
|
111
|
+
if isinstance(number_of_questions, list | tuple) and len(number_of_questions) == 1:
|
112
|
+
number_of_questions = number_of_questions[0]
|
113
|
+
|
114
|
+
pool = Pool(folder=folder)
|
115
|
+
|
116
|
+
pool.print_questions()
|
117
|
+
questions_set = QuestionSet(questions=pool.questions) # type: ignore[arg-type]
|
118
|
+
questions_set.sample(n=number_of_questions)
|
119
|
+
exam_template = ExamTemplate.load(template_tex_path)
|
120
|
+
|
121
|
+
b = ExamBatch(
|
122
|
+
N=batch_size,
|
123
|
+
questions_set=questions_set,
|
124
|
+
exam_template=exam_template,
|
125
|
+
n=number_of_questions,
|
126
|
+
)
|
127
|
+
|
128
|
+
b.make_batch()
|
129
|
+
|
130
|
+
b.compile(clean=clean, path=out_folder)
|
131
|
+
|
132
|
+
b.save(out_folder / "exams.yaml")
|
133
|
+
|
134
|
+
b = ExamBatch.load(out_folder / "exams.yaml")
|
135
|
+
|
136
|
+
b.compile(clean=clean, path=out_folder)
|
scripts/validate.py
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
"""Script that validates a single question, or all questions inside a folder."""
|
2
|
+
|
3
|
+
import shutil
|
4
|
+
from datetime import datetime
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import click
|
8
|
+
|
9
|
+
from randex.exam import Exam, ExamTemplate, Pool, QuestionSet
|
10
|
+
|
11
|
+
|
12
|
+
@click.command(
|
13
|
+
context_settings={"help_option_names": ["--help"]},
|
14
|
+
)
|
15
|
+
@click.argument(
|
16
|
+
"folder",
|
17
|
+
type=str,
|
18
|
+
nargs=1,
|
19
|
+
required=True,
|
20
|
+
)
|
21
|
+
@click.option(
|
22
|
+
"--template-tex-path",
|
23
|
+
"-t",
|
24
|
+
type=click.Path(
|
25
|
+
exists=True,
|
26
|
+
resolve_path=True,
|
27
|
+
file_okay=True,
|
28
|
+
dir_okay=False,
|
29
|
+
path_type=Path,
|
30
|
+
),
|
31
|
+
required=True,
|
32
|
+
help="Path to the YAML file that contains the template for the exam configuration",
|
33
|
+
)
|
34
|
+
@click.option(
|
35
|
+
"--out-folder",
|
36
|
+
"-o",
|
37
|
+
type=Path,
|
38
|
+
default=".",
|
39
|
+
help="Run the latex compiler inside this folder",
|
40
|
+
)
|
41
|
+
@click.option(
|
42
|
+
"--clean",
|
43
|
+
"-c",
|
44
|
+
is_flag=True,
|
45
|
+
default=False,
|
46
|
+
help="Clean all latex compilation auxiliary files.",
|
47
|
+
)
|
48
|
+
@click.option(
|
49
|
+
"--show-answers",
|
50
|
+
"-a",
|
51
|
+
is_flag=True,
|
52
|
+
default=False,
|
53
|
+
help="Show the right answers on the pdf",
|
54
|
+
)
|
55
|
+
@click.option(
|
56
|
+
"--overwrite",
|
57
|
+
is_flag=True,
|
58
|
+
default=False,
|
59
|
+
help="Overwrite the out-folder if it already exists (use with caution).",
|
60
|
+
)
|
61
|
+
def main(
|
62
|
+
folder: Path,
|
63
|
+
template_tex_path: Path,
|
64
|
+
out_folder: Path,
|
65
|
+
clean: bool,
|
66
|
+
show_answers: bool,
|
67
|
+
overwrite: bool,
|
68
|
+
) -> None:
|
69
|
+
"""
|
70
|
+
Create a pdf file with all the questions defined in FOLDER.
|
71
|
+
|
72
|
+
The FOLDER is traversed recursively.
|
73
|
+
"""
|
74
|
+
if out_folder is None:
|
75
|
+
out_folder = Path(f"tmp_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}")
|
76
|
+
|
77
|
+
if out_folder.exists():
|
78
|
+
if not overwrite:
|
79
|
+
raise click.UsageError(
|
80
|
+
f"Output folder '{out_folder}' already exists.\n"
|
81
|
+
"Use --overwrite to remove it and continue.",
|
82
|
+
)
|
83
|
+
shutil.rmtree(out_folder)
|
84
|
+
|
85
|
+
pool = Pool(folder=folder)
|
86
|
+
|
87
|
+
pool.print_questions()
|
88
|
+
questions_set = QuestionSet(questions=pool.questions) # type: ignore[arg-type]
|
89
|
+
number_of_questions = questions_set.size()
|
90
|
+
questions = questions_set.sample(n=number_of_questions)
|
91
|
+
exam_template = ExamTemplate.load(template_tex_path)
|
92
|
+
|
93
|
+
exam = Exam(
|
94
|
+
exam_template=exam_template,
|
95
|
+
questions=questions,
|
96
|
+
show_answers=show_answers,
|
97
|
+
)
|
98
|
+
exam.apply_shuffling(shuffle_questions=True, shuffle_answers=True)
|
99
|
+
|
100
|
+
result = exam.compile(path=out_folder, clean=clean)
|
101
|
+
|
102
|
+
print("STDOUT:")
|
103
|
+
print("\n\t".join(result.stdout.splitlines()))
|
104
|
+
print("STDERR:")
|
105
|
+
print("\n\t".join(result.stderr.splitlines()))
|