mcqpy-core 0.2.2__tar.gz → 0.2.3__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 (54) hide show
  1. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/PKG-INFO +1 -1
  2. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/pyproject.toml +1 -1
  3. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/__init__.py +10 -1
  4. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/overall_analysis.py +4 -1
  5. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/grader.py +12 -8
  6. mcqpy_core-0.2.2/src/mcqpy_core/grading/types.py → mcqpy_core-0.2.3/src/mcqpy_core/grading/helpers.py +1 -35
  7. mcqpy_core-0.2.3/src/mcqpy_core/grading/types.py +56 -0
  8. mcqpy_core-0.2.2/src/mcqpy_core/cli/build.py +0 -89
  9. mcqpy_core-0.2.2/src/mcqpy_core/cli/check_latex.py +0 -31
  10. mcqpy_core-0.2.2/src/mcqpy_core/cli/grade.py +0 -100
  11. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/README.md +0 -0
  12. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/__init__.py +0 -0
  13. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/__init__.py +0 -0
  14. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/_selection.py +0 -0
  15. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/config.py +0 -0
  16. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/__init__.py +0 -0
  17. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/main.py +0 -0
  18. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/token.py +0 -0
  19. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/export/web.py +0 -0
  20. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/init.py +0 -0
  21. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/main.py +0 -0
  22. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/__init__.py +0 -0
  23. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/check_tags.py +0 -0
  24. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/init.py +0 -0
  25. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/main.py +0 -0
  26. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/render.py +0 -0
  27. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/question/validate.py +0 -0
  28. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/__init__.py +0 -0
  29. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/autofill.py +0 -0
  30. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/check_filter.py +0 -0
  31. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/cli/utils/main.py +0 -0
  32. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/front_config.py +0 -0
  33. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/__init__.py +0 -0
  34. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/question_analysis.py +0 -0
  35. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/analysis/utils.py +0 -0
  36. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/grading/rubric.py +0 -0
  37. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/header_config.py +0 -0
  38. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/manifest.py +0 -0
  39. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/__init__.py +0 -0
  40. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/__init__.py +0 -0
  41. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/base_filter.py +0 -0
  42. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/date.py +0 -0
  43. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/difficulty.py +0 -0
  44. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/factory.py +0 -0
  45. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/manifest.py +0 -0
  46. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/slug.py +0 -0
  47. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/stratified.py +0 -0
  48. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/filter/tag.py +0 -0
  49. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/question.py +0 -0
  50. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/question_bank.py +0 -0
  51. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/question/utils.py +0 -0
  52. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/__init__.py +0 -0
  53. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/bundle.py +0 -0
  54. {mcqpy_core-0.2.2 → mcqpy_core-0.2.3}/src/mcqpy_core/web/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mcqpy-core
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Core question, manifest, grading, analysis, and browser-safe runtime logic for MCQPy
5
5
  Author: Mads-Peter
6
6
  Author-email: Mads-Peter <machri@phys.au.dk>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcqpy-core"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Core question, manifest, grading, analysis, and browser-safe runtime logic for MCQPy"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -7,9 +7,14 @@ from .types import (
7
7
  GradedSet,
8
8
  ParsedQuestion,
9
9
  ParsedSet,
10
- get_grade_dataframe,
10
+ GradeResult,
11
+ GradeState,
12
+ ParseResult,
13
+ ParseState,
11
14
  )
12
15
 
16
+ from .helpers import get_grade_dataframe
17
+
13
18
  __all__ = [
14
19
  "MCQGrader",
15
20
  "Rubric",
@@ -18,6 +23,10 @@ __all__ = [
18
23
  "ParsedSet",
19
24
  "GradedQuestion",
20
25
  "GradedSet",
26
+ "GradeResult",
27
+ "GradeState",
28
+ "ParseResult",
29
+ "ParseState",
21
30
  "get_grade_dataframe",
22
31
  "grade_parsed_set",
23
32
  ]
@@ -9,7 +9,10 @@ from scipy.stats import pearsonr
9
9
  from mcqpy_core.grading.analysis.utils import AnalysisFigure
10
10
 
11
11
  def get_points_array(graded_sets: list[GradedSet]):
12
- point_arrays = np.zeros((len(graded_sets), len(graded_sets[0].graded_questions)))
12
+
13
+ n_questions = max(len(graded_set.graded_questions) for graded_set in graded_sets)
14
+
15
+ point_arrays = np.zeros((len(graded_sets), n_questions))
13
16
  for i, graded_set in enumerate(graded_sets):
14
17
  for j, question in enumerate(graded_set.graded_questions):
15
18
  point_arrays[i, j] = question.point_value
@@ -1,13 +1,14 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from mcqpy_core.manifest import Manifest
4
- from mcqpy_core.grading.types import GradedQuestion, GradedSet, ParsedSet
4
+ from mcqpy_core.grading.types import GradedQuestion, GradedSet, ParsedSet, GradeResult, GradeState
5
5
  from mcqpy_core.grading.rubric import Rubric
6
6
 
7
7
 
8
- def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet) -> GradedSet:
8
+ def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet) -> GradeResult:
9
9
  graded_set = GradedSet(student_info=parsed_set.student_info, graded_questions=[])
10
10
 
11
+ other_info = {}
11
12
  for parsed_question in parsed_set.questions:
12
13
  manifest_item = manifest.get_item_by_qid(parsed_question.qid)
13
14
 
@@ -19,17 +20,20 @@ def grade_parsed_set(manifest: Manifest, rubric: Rubric, parsed_set: ParsedSet)
19
20
  max_point_value=manifest_item.point_value,
20
21
  )
21
22
 
22
- if sum(graded_question.student_answers) == 0:
23
- raise Warning(
24
- f"No answers provided for question {graded_question.qid} ({graded_question.slug})"
25
- )
23
+ if sum(parsed_question.onehot) == 0:
24
+ message = f"No answers provided for question {parsed_question.qid} ({parsed_question.slug})."
25
+ other_info[parsed_question.qid] = message
26
26
 
27
27
  graded_question.point_value = rubric.score_question(graded_question)
28
28
  graded_set.graded_questions.append(graded_question)
29
29
 
30
30
  graded_set.points = sum(q.point_value for q in graded_set.graded_questions)
31
31
  graded_set.max_points = sum(q.max_point_value for q in graded_set.graded_questions)
32
- return graded_set
32
+ return GradeResult(
33
+ state=GradeState.SUCCESS,
34
+ graded_set=graded_set,
35
+ other_info=other_info if other_info else None,
36
+ )
33
37
 
34
38
 
35
39
  class MCQGrader:
@@ -38,7 +42,7 @@ class MCQGrader:
38
42
  self.rubric = rubric
39
43
  self.regex_pattern = regex_pattern
40
44
 
41
- def grade(self, parsed_set: ParsedSet | None = None, student_answer: str | Path = None) -> GradedSet:
45
+ def grade(self, parsed_set: ParsedSet | None = None, student_answer: str | Path = None) -> GradeResult:
42
46
  if parsed_set is None:
43
47
  raise ValueError("MCQGrader.grade requires a ParsedSet in mcqpy-core")
44
48
  return grade_parsed_set(self.manifest, self.rubric, parsed_set)
@@ -1,38 +1,4 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- @dataclass
5
- class ParsedQuestion:
6
- qid: str
7
- slug: str
8
- answers: list[int]
9
- onehot: list[int]
10
-
11
-
12
- @dataclass
13
- class ParsedSet:
14
- student_info: dict[str, str]
15
- questions: list[ParsedQuestion]
16
- file: str | None = None
17
-
18
-
19
- @dataclass
20
- class GradedQuestion:
21
- qid: str
22
- slug: str
23
- student_answers: list[int]
24
- correct_answers: list[int]
25
- max_point_value: int
26
- point_value: int = 0
27
-
28
-
29
- @dataclass
30
- class GradedSet:
31
- student_info: dict[str, str]
32
- graded_questions: list[GradedQuestion]
33
- points: int = 0
34
- max_points: int = 0
35
-
1
+ from mcqpy_core.grading.types import GradedSet
36
2
 
37
3
  def get_grade_dataframe(
38
4
  graded_sets: list[GradedSet], sort_key: str | None = None
@@ -0,0 +1,56 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ @dataclass
5
+ class ParsedQuestion:
6
+ qid: str
7
+ slug: str
8
+ answers: list[int]
9
+ onehot: list[int]
10
+
11
+
12
+ @dataclass
13
+ class ParsedSet:
14
+ student_info: dict[str, str]
15
+ questions: list[ParsedQuestion]
16
+ file: str | None = None
17
+
18
+
19
+ @dataclass
20
+ class GradedQuestion:
21
+ qid: str
22
+ slug: str
23
+ student_answers: list[int]
24
+ correct_answers: list[int]
25
+ max_point_value: int
26
+ point_value: int = 0
27
+
28
+
29
+ @dataclass
30
+ class GradedSet:
31
+ student_info: dict[str, str]
32
+ graded_questions: list[GradedQuestion]
33
+ points: int = 0
34
+ max_points: int = 0
35
+
36
+ class ParseState(Enum):
37
+ READER_ERROR = "Error reading PDF file. The file may be corrupted or not a valid PDF."
38
+ SUCCESS = "Successfully parsed PDF file."
39
+
40
+ class GradeState(Enum):
41
+ READER_ERROR = ParseState.READER_ERROR.value
42
+ SUCCESS = "Successfully graded the parsed set."
43
+
44
+ @dataclass
45
+ class ParseResult:
46
+ state: ParseState
47
+ parsed_set: ParsedSet | None = None
48
+ error_message: str | None = None
49
+ other_info: dict | None = None
50
+
51
+ @dataclass
52
+ class GradeResult:
53
+ state: GradeState
54
+ graded_set: GradedSet | None = None
55
+ error_message: str | None = None
56
+ other_info: dict | None = None
@@ -1,89 +0,0 @@
1
- """
2
- CLI Build Command
3
- """
4
-
5
- from pathlib import Path
6
-
7
- import rich_click as click
8
- from rich.pretty import Pretty
9
- from rich.console import Console
10
-
11
- from mcqpy_core.cli._selection import select_questions, _build_filter, build_filters
12
- from mcqpy_core.cli.config import QuizConfig
13
- from mcqpy_core.cli.main import main
14
- from mcqpy_pdf.compile import MultipleChoiceQuiz
15
- from mcqpy_core.manifest import Manifest
16
- from mcqpy_core.question import QuestionBank
17
-
18
-
19
- # Backward-compatible private alias kept for older tests/importers.
20
- _select_questions = select_questions
21
-
22
-
23
- def build_solution(questions, manifest, output_path: Path):
24
- from mcqpy_pdf.compile.solution_pdf import SolutionPDF
25
-
26
- solution_pdf = SolutionPDF(file=output_path, questions=questions, manifest=manifest)
27
- solution_pdf.build(generate_pdf=True)
28
-
29
- @main.command(
30
- name="build",
31
- help="""Build the quiz PDF from question files based on the provided configuration.
32
- This command reads the quiz configuration, selects questions from the question bank,
33
- and generates the quiz PDF along with a solution PDF.
34
- """,
35
- )
36
- @click.option(
37
- "-c",
38
- "--config",
39
- type=click.Path(exists=True, path_type=Path),
40
- default="config.yaml",
41
- help="Path to the config file",
42
- show_default=True,
43
- )
44
- def build_command(config):
45
- """
46
- Build the quiz PDF from question files based on the provided configuration.
47
- This command reads the quiz configuration, selects questions from the question bank,
48
- and generates the quiz PDF along with a solution PDF.
49
-
50
- ```
51
- mcqpy build --config config.yaml
52
- ```
53
- """
54
-
55
- config = QuizConfig.read_yaml(config)
56
- question_bank = QuestionBank.from_directories(
57
- config.questions_paths, seed=config.selection.seed
58
- )
59
- questions = select_questions(question_bank, config.selection)
60
-
61
- console = Console()
62
- console.print("[bold green]Quiz Configuration:[/bold green]")
63
- console.print(Pretty(config))
64
- console.print(
65
- f"[bold green]Total questions in bank:[/bold green] {len(question_bank)}"
66
- )
67
- console.print(f"[bold green]Selected questions:[/bold green] {len(questions)}")
68
-
69
- ## Paths:
70
- for path in [config.root_directory, config.output_directory, config.submission_directory]:
71
- if path and not path.exists():
72
- path.mkdir(parents=True, exist_ok=True) # pragma: no cover
73
-
74
- mcq = MultipleChoiceQuiz(
75
- file=config.file_path,
76
- questions=questions,
77
- front_matter=config.front_matter,
78
- header_footer=config.header,
79
- )
80
-
81
- mcq.build(generate_pdf=True)
82
-
83
- # Build solution PDF
84
- manifest_path = mcq.get_manifest_path()
85
- manifest = Manifest.load_from_file(manifest_path)
86
- solution_output_path = (
87
- config.output_directory / f"{config.file_name.replace('.pdf', '')}_solution.pdf"
88
- )
89
- build_solution(questions, manifest, solution_output_path)
@@ -1,31 +0,0 @@
1
- """
2
- CLI command to check LaTeX installation and configuration.
3
- """
4
-
5
- from mcqpy_core.cli import main
6
-
7
- @main.command('check-latex', help="Check LaTeX installation and configuration.")
8
- def check_latex_command():
9
- """
10
- Check if LaTeX is installed and properly configured.
11
- Provides the CLI command:
12
- ```
13
- mcqpy check-latex
14
- ```
15
- This command verifies the presence of LaTeX tools such as `pdflatex` and `latexmk`,
16
- and attempts to compile a simple LaTeX document to ensure everything is functioning correctly.
17
- """
18
-
19
- from mcqpy_pdf.utils.check_latex import check_latex_installation
20
- from rich.console import Console
21
-
22
- console = Console()
23
- success, details = check_latex_installation()
24
-
25
- if success:
26
- console.print("[bold green]✓ LaTeX is properly installed![/bold green]")
27
- console.print(f"[green]pdflatex version[/green]: {details['pdflatex'].version}")
28
- console.print(f"[green]latexmk version[/green]: {details['latexmk'].version}")
29
- console.print("[green]Compilation test passed successfully.[/green]")
30
- else:
31
- console.print(f"[bold red]✗ LaTeX installation issue: {details['error_message']}[/bold red]")
@@ -1,100 +0,0 @@
1
- """Grade student submissions from a quiz."""
2
-
3
- import rich_click as click
4
- from mcqpy_core.cli.main import main
5
- from mcqpy_core.cli.config import QuizConfig
6
- from pathlib import Path
7
-
8
- from mcqpy_core.grading import MCQGrader, get_grade_dataframe
9
- from mcqpy_core.manifest import Manifest
10
- from mcqpy_core.grading.rubric import StrictRubric
11
- from rich.progress import track
12
-
13
- from mcqpy_core.question.question_bank import QuestionBank
14
-
15
-
16
- @main.command(
17
- name="grade",
18
- help="""Grade student submissions.
19
- Generates a grade report in the specified format (Excel or CSV).
20
- Students submissions are expected to be in submission directory specified in the config file.
21
- """,
22
- )
23
- @click.option(
24
- "-c",
25
- "--config",
26
- type=click.Path(exists=True, path_type=Path),
27
- default="config.yaml",
28
- help="Path to the config file",
29
- show_default=True,
30
- )
31
- @click.option("-v", "--verbose", is_flag=True, help="Enable verbose output")
32
- @click.option(
33
- "-f",
34
- "--file-format",
35
- type=click.Choice(["xlsx", "csv"]),
36
- default="xlsx",
37
- help="Output format for the grades",
38
- show_default=True,
39
- )
40
- @click.option(
41
- "-a",
42
- "--analysis",
43
- is_flag=True,
44
- help="Generate question analysis reports",
45
- default=False,
46
- )
47
- def grade_command(config, verbose: bool, file_format: str, analysis: bool):
48
- """
49
- Grade student submissions based on the provided configuration file.
50
- Generates a grade report in the specified format (Excel or CSV).
51
- Students submissions are expected to be in submission directory specified in the config file.
52
-
53
- Provides the CLI command:
54
- ```
55
- mcqpy grade --config config.yaml --file-format xlsx --analysis
56
- ```
57
- """
58
- # Load config
59
- config = QuizConfig.read_yaml(config)
60
- manifest_path = config.output_directory / f"{config.file_path.stem}_manifest.json"
61
- manifest = Manifest.load_from_file(manifest_path)
62
-
63
- # Read & Grade submissions
64
- graded_sets = []
65
- grader = MCQGrader(
66
- manifest, StrictRubric(), regex_pattern=config.grading.anonymous_pattern
67
- )
68
- submissions = list(config.submission_directory.glob("*.pdf"))
69
- for submission in track(
70
- submissions,
71
- description=f"Grading submissions ({len(submissions)})",
72
- total=len(submissions),
73
- ):
74
- graded_set = grader.grade(submission)
75
- graded_sets.append(graded_set)
76
-
77
- # Export grades to dataframe
78
- df = get_grade_dataframe(graded_sets, sort_key=config.grading.output_sort_key)
79
- output_path = (config.root_directory / f"{config.file_path.stem}_grades.{file_format}")
80
- if file_format == "xlsx":
81
- df.to_excel(output_path, index=False)
82
- elif file_format == "csv":
83
- df.to_csv(output_path, index=False)
84
-
85
- if analysis:
86
- from mcqpy_pdf.analysis.report import QuizAnalysis
87
-
88
- analysis_directory = config.root_directory / "analysis/"
89
- analysis_directory.mkdir(exist_ok=True)
90
-
91
- question_bank = QuestionBank.from_directories(config.questions_paths)
92
- print(f"Question bank loaded for analysis - {len(question_bank)}")
93
-
94
- quiz_analysis = QuizAnalysis(
95
- graded_sets,
96
- question_bank=question_bank,
97
- output_dir=analysis_directory,
98
- grading_config=config.grading,
99
- )
100
- quiz_analysis.build()
File without changes