randex 0.2.1__py3-none-any.whl → 0.3.2__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/cli.py CHANGED
@@ -1,8 +1,82 @@
1
1
  """Shared CLI utilities for randex scripts."""
2
2
 
3
+ import logging
4
+ import sys
5
+
3
6
  import click
4
7
 
5
8
 
9
+ def setup_logging(verbose: int = 0, quiet: bool = False) -> None:
10
+ """
11
+ Set up logging configuration for the CLI.
12
+
13
+ Parameters
14
+ ----------
15
+ verbose : int
16
+ Verbosity level (0=INFO, 1=DEBUG, 2+ more verbose DEBUG)
17
+ quiet : bool
18
+ If True, only show warnings and errors
19
+ """
20
+ if quiet:
21
+ level = logging.WARNING
22
+ elif verbose >= 1:
23
+ level = logging.DEBUG
24
+ else:
25
+ level = logging.INFO
26
+
27
+ # Create formatter
28
+ formatter = logging.Formatter(fmt="%(message)s", datefmt="%Y-%m-%d %H:%M:%S")
29
+
30
+ # Set up the root logger
31
+ logger = logging.getLogger()
32
+ logger.setLevel(level)
33
+
34
+ # Remove any existing handlers
35
+ for handler in logger.handlers[:]:
36
+ logger.removeHandler(handler)
37
+
38
+ # Create console handler
39
+ handler = logging.StreamHandler(sys.stdout)
40
+ handler.setLevel(level)
41
+ handler.setFormatter(formatter)
42
+
43
+ logger.addHandler(handler)
44
+
45
+ # For verbose mode, also show logger names and levels
46
+ if verbose >= 2:
47
+ formatter = logging.Formatter(
48
+ fmt="[%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
49
+ )
50
+ handler.setFormatter(formatter)
51
+
52
+
53
+ def get_logger(name: str | None = None) -> logging.Logger:
54
+ """
55
+ Get a logger instance.
56
+
57
+ Parameters
58
+ ----------
59
+ name : str, optional
60
+ Logger name. If None, uses the calling module name.
61
+
62
+ Returns
63
+ -------
64
+ logging.Logger
65
+ Configured logger instance
66
+ """
67
+ if name is None:
68
+ # Get the calling module name
69
+ import inspect
70
+
71
+ frame = inspect.currentframe()
72
+ if frame and frame.f_back:
73
+ name = frame.f_back.f_globals.get("__name__", "randex")
74
+ else:
75
+ name = "randex"
76
+
77
+ return logging.getLogger(name)
78
+
79
+
6
80
  class CustomCommand(click.Command):
7
81
  """Custom Click command that provides better error messages."""
8
82
 
randex/exam.py CHANGED
@@ -299,6 +299,10 @@ class Pool:
299
299
 
300
300
  def print_questions(self) -> None:
301
301
  """Print all questions in the pool grouped by folder."""
302
+ from randex.cli import get_logger
303
+
304
+ logger = get_logger(__name__)
305
+
302
306
  first = True
303
307
  for folder, question_list in sorted(
304
308
  self.questions.items(),
@@ -308,18 +312,19 @@ class Pool:
308
312
  continue
309
313
 
310
314
  if not first:
311
- print("\n" + "-" * 60)
315
+ s = "\n" + "-" * 60
316
+ logger.info(s)
312
317
  first = False
313
318
 
314
- print(f"\n\U0001f4c1 {folder}\n")
319
+ logger.info("\n\U0001f4c1 %s\n", folder)
315
320
  for i, question in enumerate(question_list):
316
- print(f" \U0001f4c4 Question {i + 1}")
317
- print(f" Q: {question.question}")
318
- print(" Answers:")
321
+ logger.info(" \U0001f4c4 Question %d", i + 1)
322
+ logger.info(" Q: %s", question.question)
323
+ logger.info(" Answers:")
319
324
  for j, ans in enumerate(question.answers):
320
325
  mark = "✅" if j == question.right_answer else " "
321
- print(f" {mark} {j}. {ans}")
322
- print("\n")
326
+ logger.info(" %s %d. %s", mark, j, ans)
327
+ logger.info("\n")
323
328
 
324
329
 
325
330
  class QuestionSet(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: randex
3
- Version: 0.2.1
3
+ Version: 0.3.2
4
4
  Summary: Create randomized multiple choice exams using latex.
5
5
  License: CC BY-NC 4.0
6
6
  Author: G. Arampatzis
@@ -70,57 +70,56 @@ probably have already `latexmk` installed as well.
70
70
 
71
71
  ## Randex Commands
72
72
 
73
- ### randex-download-examples
73
+ ### randex download-examples
74
74
 
75
- To download the latest examples from GitHub, run the following command from the root
76
- folder of the project:
75
+ To download the latest examples from GitHub, run the following command:
77
76
 
78
77
  ```sh
79
- randex-download-examples
78
+ randex download-examples
80
79
  ```
81
80
 
82
- ### validate
81
+ ### randex validate
83
82
 
84
83
  This command validates a single question or all questions under a folder. Execute:
85
84
 
86
85
  ```sh
87
- validate -t examples/en/template-exam.yaml -o tmp --overwrite "examples/en/folder_*"
86
+ randex validate "examples/en/folder_*" -t examples/en/template-exam.yaml -o tmp --overwrite
88
87
  ```
89
88
 
90
89
  to validate all the questions under the folder `examples/en` that contains subfolders
91
90
  with questions.
92
91
  It will use the configuration from the file `examples/en/template-exam.yaml`.
93
- The LaTeX compilation will run inside the `temp` folder.
92
+ The LaTeX compilation will run inside the `tmp` folder.
94
93
  The `--clean` option will remove all intermediate files created by LaTeX,
95
94
  and the `-a` flag will show the correct answers in the produced PDF.
96
- Open the PDF file inside `temp` to validate that all questions appear correctly.
95
+ Open the PDF file inside `tmp` to validate that all questions appear correctly.
97
96
 
98
97
  Run:
99
98
 
100
99
  ```sh
101
- validate --help
100
+ randex validate --help
102
101
  ```
103
102
 
104
103
  to see the help message for the command.
105
104
 
106
- ### exams
105
+ ### randex batch
107
106
 
108
107
  To create a batch of exams with random questions, execute:
109
108
 
110
109
  ```sh
111
- exams -b 5 -n 2 -t examples/en/template-exam.yaml -o tmp --overwrite --clean "examples/en/folder_*"
110
+ randex batch "examples/en/folder_*" 5 -n 2 -t examples/en/template-exam.yaml -o tmp --overwrite --clean
112
111
  ```
113
112
 
114
- This command will create 5 exams from using the questions inside the 3 folders with
113
+ This command will create 5 exams using the questions inside the 3 folders with
115
114
  names `folder_0`, `folder_1`, and `folder_2` using the configuration from the file
116
115
  `examples/en/template-exam.yaml`.
117
116
  The `--clean` option will remove all intermediate files created by LaTeX.
118
117
  The `-n` option specifies the number of questions randomly chosen from each folder.
119
118
  It can appear once, meaning all folders will contribute the same number of questions,
120
- or multiple times, e.g., `-n 2 -n 1 -n 3 `, indicating the first folder will contribute
119
+ or multiple times, e.g., `-n 2 -n 1 -n 3`, indicating the first folder will contribute
121
120
  2 questions, the second folder will contribute 1 question, and the third folder will
122
121
  contribute 3 questions.
123
- The `-b` option specifies the number of exams to create.
122
+ The batch size (5 in this example) specifies the number of exams to create.
124
123
 
125
124
  ### Grade
126
125
 
@@ -0,0 +1,13 @@
1
+ randex/__init__.py,sha256=6bBAISO6MqVCSCvqLcK3SGUnDODuwobLWouO3GY8SRU,187
2
+ randex/cli.py,sha256=TGB8XqfNajWfKju9rhUctQM3M_CbyjrZ2LGDlY3Bw7M,3851
3
+ randex/exam.py,sha256=4XAwz9Vv4ej0iza2bxzirLVnHgHpHUQhKTGJgxi_1as,27129
4
+ scripts/__init__.py,sha256=F8pjiW8AlL0IhnvVh1TEfgiD--YdFE_PggtSevHp5y4,38
5
+ scripts/batch.py,sha256=ceRZM4qf6_gVdVWKktsgmGp65iyM1jxorjEpOGdXrRQ,3159
6
+ scripts/download_examples.py,sha256=wpe_d7QR1iHhPOL5LBawBb1Feu4DnScyGiaMTMAyqRQ,2103
7
+ scripts/randex.py,sha256=92bbt6ovheWKsbeHwUjKZeK_jv35pFs4a6AygLrbilA,5383
8
+ scripts/validate.py,sha256=ojlcme_6-H1XcxUgN-2VgP1k_kfPZSIaILPqD7L0tCw,3738
9
+ randex-0.3.2.dist-info/LICENSE,sha256=NxH5Y8BdC-gNU-WSMwim3uMbID2iNDXJz7fHtuTdXhk,19346
10
+ randex-0.3.2.dist-info/METADATA,sha256=48k9IhrC5AwvBKgmQhwMHd0nbV-aXMMPCgGvFHbS8kc,6557
11
+ randex-0.3.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
+ randex-0.3.2.dist-info/entry_points.txt,sha256=TFWMzZGZpVk-j2F7gPjrZJT0wEJ2J9P7GrKOOv9n5NM,45
13
+ randex-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ randex=scripts.randex:cli
3
+
scripts/batch.py ADDED
@@ -0,0 +1,99 @@
1
+ """Script that creates a batch of randomized exams."""
2
+
3
+ import shutil
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from pydantic import ValidationError
10
+
11
+ from randex.cli import get_logger
12
+ from randex.exam import ExamBatch, ExamTemplate, Pool, QuestionSet
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ def main(
18
+ *,
19
+ folder: str,
20
+ number_of_questions: list | int,
21
+ batch_size: int,
22
+ template_tex_path: Path,
23
+ out_folder: Path | None,
24
+ clean: bool,
25
+ overwrite: bool,
26
+ ) -> None:
27
+ """
28
+ Create a batch of exams with randomly chosen multiple choice questions.
29
+
30
+ Parameters
31
+ ----------
32
+ folder : str
33
+ Path or quoted glob (e.g. "examples/en/folder_*").
34
+ number_of_questions : list | int
35
+ Number of questions to sample.
36
+ batch_size : int
37
+ Number of exams to be created.
38
+ template_tex_path : Path
39
+ Path to the YAML file that contains the template for the exam configuration.
40
+ out_folder : Path | None
41
+ Create the batch exams in this folder (default: tmp_HH-MM-SS).
42
+ clean : bool
43
+ Clean the output folder before creating the exams.
44
+ overwrite : bool
45
+ Overwrite the output folder if it already exists.
46
+ """
47
+ if out_folder is None:
48
+ out_folder = Path(f"tmp_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}")
49
+
50
+ logger.info("📁 Output folder: %s", out_folder)
51
+
52
+ if out_folder.exists():
53
+ if not overwrite:
54
+ raise click.UsageError(
55
+ f"Output folder '{out_folder}' already exists.\n"
56
+ "Use --overwrite to remove it and continue.",
57
+ )
58
+ logger.warning("🗑️ Removing existing output folder: %s", out_folder)
59
+ shutil.rmtree(out_folder)
60
+
61
+ if isinstance(number_of_questions, list | tuple) and len(number_of_questions) == 1:
62
+ number_of_questions = number_of_questions[0]
63
+
64
+ logger.info("📂 Loading questions from: %s", folder)
65
+ pool = Pool(folder=folder)
66
+
67
+ pool.print_questions()
68
+ questions_set = QuestionSet(questions=pool.questions) # type: ignore[arg-type]
69
+ questions_set.sample(n=number_of_questions)
70
+
71
+ logger.info("📄 Loading exam template from: %s", template_tex_path)
72
+ exam_template = ExamTemplate.load(template_tex_path)
73
+
74
+ try:
75
+ logger.info("🔄 Creating batch of %d exams...", batch_size)
76
+ b = ExamBatch(
77
+ N=batch_size,
78
+ questions_set=questions_set,
79
+ exam_template=exam_template,
80
+ n=number_of_questions,
81
+ )
82
+ except ValidationError as e:
83
+ logger.exception("❌ Validation error while creating exam batch:")
84
+ logger.exception(e.json(indent=2))
85
+ sys.exit(1)
86
+
87
+ b.make_batch()
88
+
89
+ logger.info("🔨 Compiling exams...")
90
+ b.compile(clean=clean, path=out_folder)
91
+
92
+ logger.info("💾 Saving batch configuration to: %s", out_folder / "exams.yaml")
93
+ b.save(out_folder / "exams.yaml")
94
+
95
+ logger.debug("🔄 Reloading and recompiling batch...")
96
+ b = ExamBatch.load(out_folder / "exams.yaml")
97
+ b.compile(clean=clean, path=out_folder)
98
+
99
+ logger.info("✅ Batch creation completed successfully in: %s", out_folder)
@@ -9,49 +9,54 @@ import zipfile
9
9
  from pathlib import Path
10
10
  from urllib.error import URLError
11
11
 
12
+ from randex.cli import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
12
16
  GITHUB_ZIP_URL = "https://github.com/arampatzis/randex/archive/refs/heads/main.zip"
13
17
  DEST_DIR = Path("examples")
14
18
 
15
19
 
16
20
  def main() -> None:
17
21
  """Download the latest examples from GitHub."""
18
- print("📦 Downloading examples from GitHub...")
22
+ logger.info("📦 Downloading examples from GitHub...")
19
23
 
20
24
  if DEST_DIR.exists():
21
- print(
22
- f"❌ Destination folder '{DEST_DIR}' already exists. "
23
- "Please remove it first."
25
+ logger.error(
26
+ "❌ Destination folder '%s' already exists. Please remove it first."
24
27
  )
25
28
  sys.exit(1)
26
29
 
27
30
  try:
31
+ logger.debug("🌐 Fetching from: %s", GITHUB_ZIP_URL)
28
32
  with (
29
33
  urllib.request.urlopen(GITHUB_ZIP_URL) as response, # noqa: S310
30
34
  zipfile.ZipFile(io.BytesIO(response.read())) as zip_ref,
31
35
  ):
32
36
  temp_dir = Path(tempfile.mkdtemp())
37
+ logger.debug("📁 Extracting to temporary directory: %s", temp_dir)
33
38
  zip_ref.extractall(temp_dir)
34
39
 
35
40
  # Detect the root folder inside the zip (e.g., randex-main)
36
41
  root_entry = next((p for p in temp_dir.iterdir() if p.is_dir()), None)
37
42
 
38
43
  if not root_entry:
39
- print("❌ Could not locate the root folder in the archive.")
44
+ logger.error("❌ Could not locate the root folder in the archive.")
40
45
  sys.exit(1)
41
46
 
42
47
  examples_path = root_entry / "examples"
48
+ logger.debug("🔍 Looking for examples in: %s", examples_path)
43
49
 
44
50
  if not examples_path.exists():
45
- print("❌ examples/ folder not found in the downloaded repository.")
51
+ logger.error(
52
+ "❌ examples/ folder not found in the downloaded repository."
53
+ )
46
54
  sys.exit(1)
47
55
 
56
+ logger.debug("📂 Copying examples from %s to %s", examples_path, DEST_DIR)
48
57
  shutil.copytree(examples_path, DEST_DIR)
49
- print(f"✅ Examples downloaded to: {DEST_DIR.resolve()}")
58
+ logger.info("✅ Examples downloaded to: %s", DEST_DIR.resolve())
50
59
 
51
- except (URLError, zipfile.BadZipFile, OSError, shutil.Error) as e:
52
- print(f"❌ Error: {e}")
60
+ except (URLError, zipfile.BadZipFile, OSError, shutil.Error):
61
+ logger.exception("❌ Error occurred during the download process.")
53
62
  sys.exit(1)
54
-
55
-
56
- if __name__ == "__main__":
57
- main()
scripts/randex.py ADDED
@@ -0,0 +1,228 @@
1
+ """Main CLI for randex - Create randomized multiple choice exams using latex."""
2
+
3
+ import importlib.metadata
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from randex.cli import CustomCommand, setup_logging
9
+ from scripts.batch import main as batch_main
10
+ from scripts.download_examples import main as download_examples_main
11
+ from scripts.validate import main as validate_main
12
+
13
+ try:
14
+ __version__ = importlib.metadata.version("randex")
15
+ except importlib.metadata.PackageNotFoundError:
16
+ __version__ = "0.0.0"
17
+
18
+
19
+ # Main CLI group
20
+ @click.group()
21
+ @click.version_option(version=__version__, prog_name="randex")
22
+ @click.option(
23
+ "--verbose",
24
+ "-v",
25
+ count=True,
26
+ help="Increase verbosity (use -v for DEBUG, -vv for more detailed DEBUG output)",
27
+ )
28
+ @click.option(
29
+ "--quiet", "-q", is_flag=True, help="Quiet mode - only show warnings and errors"
30
+ )
31
+ @click.pass_context
32
+ def cli(ctx: click.Context, verbose: int, quiet: bool) -> None:
33
+ """randex: A CLI tool to create randomized multiple choice exams using latex."""
34
+ # Set up logging before any command runs
35
+ setup_logging(verbose=verbose, quiet=quiet)
36
+
37
+ # Store logging config in context for subcommands
38
+ ctx.ensure_object(dict)
39
+ ctx.obj["verbose"] = verbose
40
+ ctx.obj["quiet"] = quiet
41
+
42
+
43
+ # Subcommand: download-examples # noqa: ERA001
44
+ @cli.command(
45
+ cls=CustomCommand,
46
+ context_settings={"help_option_names": ["--help"]},
47
+ )
48
+ def download_examples() -> None:
49
+ """Download the latest examples from GitHub."""
50
+ download_examples_main()
51
+
52
+
53
+ @cli.command(
54
+ cls=CustomCommand,
55
+ context_settings={"help_option_names": ["--help"]},
56
+ )
57
+ @click.argument(
58
+ "folder",
59
+ type=str,
60
+ nargs=1,
61
+ required=True,
62
+ )
63
+ @click.option(
64
+ "--batch-size",
65
+ "-b",
66
+ type=int,
67
+ default=1,
68
+ help="Number of exams to be created",
69
+ )
70
+ @click.option(
71
+ "--number_of_questions",
72
+ "-n",
73
+ type=int,
74
+ default=[1],
75
+ multiple=True,
76
+ help="""
77
+ Specify how many questions to sample.
78
+
79
+ - Use once: sample total number of questions from all folders.
80
+ Example: -n 10
81
+
82
+ - Use multiple times: sample per-folder counts, in order.
83
+ Example: -n 5 -n 3 # 5 from folder 1, 3 from folder 2
84
+ """,
85
+ )
86
+ @click.option(
87
+ "--template-tex-path",
88
+ "-t",
89
+ type=click.Path(
90
+ exists=True,
91
+ resolve_path=True,
92
+ file_okay=True,
93
+ dir_okay=False,
94
+ path_type=Path,
95
+ ),
96
+ required=True,
97
+ help="Path to the YAML file that contains the template for the exam configuration",
98
+ )
99
+ @click.option(
100
+ "--out-folder",
101
+ "-o",
102
+ type=Path,
103
+ help="Create the batch exams in this folder (default: tmp_HH-MM-SS)",
104
+ )
105
+ @click.option(
106
+ "--clean",
107
+ "-c",
108
+ is_flag=True,
109
+ default=False,
110
+ help="Clean all latex compilation auxiliary files",
111
+ )
112
+ @click.option(
113
+ "--overwrite",
114
+ is_flag=True,
115
+ default=False,
116
+ help="Overwrite the out-folder if it already exists (use with caution).",
117
+ )
118
+ def batch(
119
+ folder: str,
120
+ batch_size: int,
121
+ number_of_questions: list | int,
122
+ template_tex_path: Path,
123
+ out_folder: Path | None,
124
+ clean: bool,
125
+ overwrite: bool,
126
+ ) -> None:
127
+ """
128
+ Create a batch of exams with randomly chosen multiple choice questions.
129
+
130
+ The questions are loaded from a list of FOLDERS.
131
+
132
+ FOLDER: Path or quoted glob (e.g. "data/unit_*").
133
+
134
+ The questions are loaded from the FOLDERs and must follow the format:
135
+
136
+ question: What is $1+1$?
137
+ answers: ["0", "1", "2", "3"]
138
+ right_answer: 2
139
+
140
+ 💡 Remember to wrap glob patterns in quotes to prevent shell expansion!
141
+
142
+ """
143
+ batch_main(
144
+ folder=folder,
145
+ batch_size=batch_size,
146
+ number_of_questions=number_of_questions,
147
+ template_tex_path=template_tex_path,
148
+ out_folder=out_folder,
149
+ clean=clean,
150
+ overwrite=overwrite,
151
+ )
152
+
153
+
154
+ @cli.command(
155
+ cls=CustomCommand,
156
+ context_settings={"help_option_names": ["--help"]},
157
+ )
158
+ @click.argument(
159
+ "folder",
160
+ type=str,
161
+ nargs=1,
162
+ required=True,
163
+ )
164
+ @click.option(
165
+ "--template-tex-path",
166
+ "-t",
167
+ type=click.Path(
168
+ exists=True,
169
+ resolve_path=True,
170
+ file_okay=True,
171
+ dir_okay=False,
172
+ path_type=Path,
173
+ ),
174
+ required=True,
175
+ help="Path to the YAML file that contains the template for the exam configuration",
176
+ )
177
+ @click.option(
178
+ "--out-folder",
179
+ "-o",
180
+ type=Path,
181
+ default=".",
182
+ help="Run the latex compiler inside this folder",
183
+ )
184
+ @click.option(
185
+ "--clean",
186
+ "-c",
187
+ is_flag=True,
188
+ default=False,
189
+ help="Clean all latex compilation auxiliary files.",
190
+ )
191
+ @click.option(
192
+ "--show-answers",
193
+ "-a",
194
+ is_flag=True,
195
+ default=False,
196
+ help="Show the right answers on the pdf",
197
+ )
198
+ @click.option(
199
+ "--overwrite",
200
+ is_flag=True,
201
+ default=False,
202
+ help="Overwrite the out-folder if it already exists (use with caution).",
203
+ )
204
+ def validate(
205
+ folder: str,
206
+ template_tex_path: Path,
207
+ out_folder: Path,
208
+ clean: bool,
209
+ show_answers: bool,
210
+ overwrite: bool,
211
+ ) -> None:
212
+ """
213
+ Create a pdf file with all the questions defined in FOLDER.
214
+
215
+ The FOLDER is traversed recursively to load all questions.
216
+ """
217
+ validate_main(
218
+ folder=folder,
219
+ template_tex_path=template_tex_path,
220
+ out_folder=out_folder,
221
+ clean=clean,
222
+ show_answers=show_answers,
223
+ overwrite=overwrite,
224
+ )
225
+
226
+
227
+ if __name__ == "__main__":
228
+ cli()
scripts/validate.py CHANGED
@@ -1,97 +1,121 @@
1
1
  """Script that validates a single question, or all questions inside a folder."""
2
2
 
3
+ import logging
3
4
  import shutil
5
+ import subprocess
4
6
  from datetime import datetime
5
7
  from pathlib import Path
6
8
 
7
9
  import click
8
10
 
9
- from randex.cli import CustomCommand
11
+ from randex.cli import get_logger
10
12
  from randex.exam import Exam, ExamTemplate, Pool, QuestionSet
11
13
 
14
+ logger = get_logger(__name__)
12
15
 
13
- @click.command(
14
- cls=CustomCommand,
15
- context_settings={"help_option_names": ["--help"]},
16
- )
17
- @click.argument(
18
- "folder",
19
- type=str,
20
- nargs=1,
21
- required=True,
22
- )
23
- @click.option(
24
- "--template-tex-path",
25
- "-t",
26
- type=click.Path(
27
- exists=True,
28
- resolve_path=True,
29
- file_okay=True,
30
- dir_okay=False,
31
- path_type=Path,
32
- ),
33
- required=True,
34
- help="Path to the YAML file that contains the template for the exam configuration",
35
- )
36
- @click.option(
37
- "--out-folder",
38
- "-o",
39
- type=Path,
40
- default=".",
41
- help="Run the latex compiler inside this folder",
42
- )
43
- @click.option(
44
- "--clean",
45
- "-c",
46
- is_flag=True,
47
- default=False,
48
- help="Clean all latex compilation auxiliary files.",
49
- )
50
- @click.option(
51
- "--show-answers",
52
- "-a",
53
- is_flag=True,
54
- default=False,
55
- help="Show the right answers on the pdf",
56
- )
57
- @click.option(
58
- "--overwrite",
59
- is_flag=True,
60
- default=False,
61
- help="Overwrite the out-folder if it already exists (use with caution).",
62
- )
63
- def main(
64
- folder: Path,
65
- template_tex_path: Path,
66
- out_folder: Path,
67
- clean: bool,
68
- show_answers: bool,
69
- overwrite: bool,
70
- ) -> None:
16
+
17
+ def _setup_output_folder(out_folder: Path | str | None, overwrite: bool) -> Path:
71
18
  """
72
- Create a pdf file with all the questions defined in FOLDER.
19
+ Set up the output folder for validation.
73
20
 
74
- The FOLDER is traversed recursively.
21
+ Parameters
22
+ ----------
23
+ out_folder : Path | None
24
+ The output folder path, or None to generate a default.
25
+ overwrite : bool
26
+ Whether to overwrite existing folders.
27
+
28
+ Returns
29
+ -------
30
+ Path
31
+ The prepared output folder path.
75
32
  """
76
33
  if out_folder is None:
77
34
  out_folder = Path(f"tmp_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}")
78
35
 
36
+ out_folder = Path(out_folder)
37
+
38
+ logger.info("📁 Output folder: %s", out_folder)
39
+
79
40
  if out_folder.exists():
80
41
  if not overwrite:
81
42
  raise click.UsageError(
82
43
  f"Output folder '{out_folder}' already exists.\n"
83
44
  "Use --overwrite to remove it and continue.",
84
45
  )
46
+ logger.warning("🗑️ Removing existing output folder: %s", out_folder)
85
47
  shutil.rmtree(out_folder)
86
48
 
49
+ return out_folder
50
+
51
+
52
+ def _handle_compilation_result(
53
+ result: subprocess.CompletedProcess,
54
+ out_folder: Path | str,
55
+ ) -> None:
56
+ """
57
+ Handle and log the compilation results.
58
+
59
+ Parameters
60
+ ----------
61
+ result : subprocess.CompletedProcess
62
+ The compilation result from LaTeX.
63
+ out_folder : Path
64
+ The output folder path for success messages.
65
+ """
66
+ if result.stdout:
67
+ logger.debug("LaTeX compilation STDOUT:")
68
+ for line in result.stdout.splitlines():
69
+ logger.debug(" %s", line)
70
+
71
+ if result.stderr:
72
+ if result.returncode != 0:
73
+ logger.error("LaTeX compilation STDERR:")
74
+ for line in result.stderr.splitlines():
75
+ logger.error(" %s", line)
76
+ else:
77
+ logger.debug("LaTeX compilation STDERR:")
78
+ for line in result.stderr.splitlines():
79
+ logger.debug(" %s", line)
80
+
81
+ if result.returncode == 0:
82
+ logger.info("✅ Validation completed successfully in: %s", out_folder)
83
+ else:
84
+ logger.error(
85
+ "❌ LaTeX compilation failed with return code: %d", result.returncode
86
+ )
87
+
88
+
89
+ def main(
90
+ *,
91
+ folder: Path | str,
92
+ template_tex_path: Path | str,
93
+ out_folder: Path | str | None,
94
+ clean: bool,
95
+ show_answers: bool,
96
+ overwrite: bool,
97
+ ) -> None:
98
+ """
99
+ Create a pdf file with all the questions defined in FOLDER.
100
+
101
+ The FOLDER is traversed recursively.
102
+ """
103
+ out_folder = _setup_output_folder(out_folder, overwrite)
104
+
105
+ logger.info("📂 Loading questions from: %s", folder)
87
106
  pool = Pool(folder=folder)
88
107
 
89
- pool.print_questions()
108
+ if logger.isEnabledFor(logging.DEBUG):
109
+ pool.print_questions()
110
+
90
111
  questions_set = QuestionSet(questions=pool.questions) # type: ignore[arg-type]
91
112
  number_of_questions = questions_set.size()
92
113
  questions = questions_set.sample(n=number_of_questions)
93
- exam_template = ExamTemplate.load(template_tex_path)
94
114
 
115
+ logger.info("📄 Loading exam template from: %s", template_tex_path)
116
+ exam_template = ExamTemplate.load(Path(template_tex_path))
117
+
118
+ logger.info("📝 Creating validation exam with %d questions", number_of_questions)
95
119
  exam = Exam(
96
120
  exam_template=exam_template,
97
121
  questions=questions,
@@ -99,9 +123,7 @@ def main(
99
123
  )
100
124
  exam.apply_shuffling(shuffle_questions=True, shuffle_answers=True)
101
125
 
126
+ logger.info("🔨 Compiling exam...")
102
127
  result = exam.compile(path=out_folder, clean=clean)
103
128
 
104
- print("STDOUT:")
105
- print("\n\t".join(result.stdout.splitlines()))
106
- print("STDERR:")
107
- print("\n\t".join(result.stderr.splitlines()))
129
+ _handle_compilation_result(result, out_folder)
@@ -1,12 +0,0 @@
1
- randex/__init__.py,sha256=6bBAISO6MqVCSCvqLcK3SGUnDODuwobLWouO3GY8SRU,187
2
- randex/cli.py,sha256=hwLgZIfBkAeFXlbveywmQlKBGz9GsD_P6Q89YSDOe2M,2039
3
- randex/exam.py,sha256=sYBQhYjmZDNyTicSD6nOOTo_7c_rst5rhyjCJhCbVtk,26975
4
- scripts/__init__.py,sha256=F8pjiW8AlL0IhnvVh1TEfgiD--YdFE_PggtSevHp5y4,38
5
- scripts/exams.py,sha256=D4ky-3J4yx7R3_-UgpGT7C9nQWFqNvdn-fhvzt9Jkbg,3401
6
- scripts/randex_download_examples.py,sha256=fj88XkZV8bpHPCeXu1f3nVNws_hbcJ_3Zso7XOPB0RQ,1682
7
- scripts/validate.py,sha256=MnJqfSs8n6qSuxMUT4W0P4ePGyDtRUIXKakWVy20eVo,2659
8
- randex-0.2.1.dist-info/LICENSE,sha256=NxH5Y8BdC-gNU-WSMwim3uMbID2iNDXJz7fHtuTdXhk,19346
9
- randex-0.2.1.dist-info/METADATA,sha256=18RJ4FY6RmVK4C9OxDWM8Qe8kY0SClXtbnXT8VrRYP8,6551
10
- randex-0.2.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
11
- randex-0.2.1.dist-info/entry_points.txt,sha256=RayNUArGNT0MLZi7uh1MHjnd1L1V4X6C4sBdMLbvAfc,138
12
- randex-0.2.1.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- [console_scripts]
2
- exams=scripts.exams:main
3
- randex-download-examples=scripts.randex_download_examples:main
4
- validate=scripts.validate:main
5
-
scripts/exams.py DELETED
@@ -1,138 +0,0 @@
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.cli import CustomCommand
10
- from randex.exam import ExamBatch, ExamTemplate, Pool, QuestionSet
11
-
12
-
13
- @click.command(
14
- cls=CustomCommand,
15
- context_settings={"help_option_names": ["--help"]},
16
- )
17
- @click.argument(
18
- "folder",
19
- type=str,
20
- nargs=1,
21
- required=True,
22
- )
23
- @click.option(
24
- "--batch-size",
25
- "-b",
26
- type=int,
27
- default=1,
28
- help="Number of exams to be created",
29
- )
30
- @click.option(
31
- "--number_of_questions",
32
- "-n",
33
- type=int,
34
- default=[1],
35
- multiple=True,
36
- help="""
37
- Specify how many questions to sample.
38
-
39
- - Use once: sample total number of questions from all folders.
40
- Example: -n 10
41
-
42
- - Use multiple times: sample per-folder counts, in order.
43
- Example: -n 5 -n 3 # 5 from folder 1, 3 from folder 2
44
- """,
45
- )
46
- @click.option(
47
- "--template-tex-path",
48
- "-t",
49
- type=click.Path(
50
- exists=True,
51
- resolve_path=True,
52
- file_okay=True,
53
- dir_okay=False,
54
- path_type=Path,
55
- ),
56
- required=True,
57
- help="Path to the YAML file that contains the template for the exam configuration",
58
- )
59
- @click.option(
60
- "--out-folder",
61
- "-o",
62
- type=Path,
63
- help="Create the batch exams in this folder (default: tmp_HH-MM-SS)",
64
- )
65
- @click.option(
66
- "--clean",
67
- "-c",
68
- is_flag=True,
69
- default=False,
70
- help="Clean all latex compilation auxiliary files",
71
- )
72
- @click.option(
73
- "--overwrite",
74
- is_flag=True,
75
- default=False,
76
- help="Overwrite the out-folder if it already exists (use with caution).",
77
- )
78
- def main(
79
- folder: str,
80
- number_of_questions: list | int,
81
- batch_size: int,
82
- template_tex_path: Path,
83
- out_folder: Path | None,
84
- clean: bool,
85
- overwrite: bool,
86
- ) -> None:
87
- """
88
- Create a batch of exams with randomly chosen multiple choice questions.
89
-
90
- The questions are loaded from a list of FOLDERS.
91
-
92
- FOLDER: Path or quoted glob (e.g. "data/unit_*").
93
-
94
- The questions are loaded from the FOLDERs and must follow the format:
95
-
96
- question: What is $1+1$?
97
- answers: ["0", "1", "2", "3"]
98
- right_answer: 2
99
-
100
- 💡 Remember to wrap glob patterns in quotes to prevent shell expansion!
101
- """
102
- if out_folder is None:
103
- out_folder = Path(f"tmp_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}")
104
-
105
- if out_folder.exists():
106
- if not overwrite:
107
- raise click.UsageError(
108
- f"Output folder '{out_folder}' already exists.\n"
109
- "Use --overwrite to remove it and continue.",
110
- )
111
- shutil.rmtree(out_folder)
112
-
113
- if isinstance(number_of_questions, list | tuple) and len(number_of_questions) == 1:
114
- number_of_questions = number_of_questions[0]
115
-
116
- pool = Pool(folder=folder)
117
-
118
- pool.print_questions()
119
- questions_set = QuestionSet(questions=pool.questions) # type: ignore[arg-type]
120
- questions_set.sample(n=number_of_questions)
121
- exam_template = ExamTemplate.load(template_tex_path)
122
-
123
- b = ExamBatch(
124
- N=batch_size,
125
- questions_set=questions_set,
126
- exam_template=exam_template,
127
- n=number_of_questions,
128
- )
129
-
130
- b.make_batch()
131
-
132
- b.compile(clean=clean, path=out_folder)
133
-
134
- b.save(out_folder / "exams.yaml")
135
-
136
- b = ExamBatch.load(out_folder / "exams.yaml")
137
-
138
- b.compile(clean=clean, path=out_folder)
File without changes