pyaota 0.1.0__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.
- pyaota-0.1.0/.gitignore +2 -0
- pyaota-0.1.0/LICENSE +21 -0
- pyaota-0.1.0/PKG-INFO +50 -0
- pyaota-0.1.0/README.md +4 -0
- pyaota-0.1.0/pyproject.toml +66 -0
- pyaota-0.1.0/src/pyaota/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/cli.py +397 -0
- pyaota-0.1.0/src/pyaota/data/models/mnist_digit_cnn.keras +0 -0
- pyaota-0.1.0/src/pyaota/generator/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/generator/answersheet.py +424 -0
- pyaota-0.1.0/src/pyaota/generator/document.py +83 -0
- pyaota-0.1.0/src/pyaota/generator/manager.py +334 -0
- pyaota-0.1.0/src/pyaota/generator/questionset.py +176 -0
- pyaota-0.1.0/src/pyaota/generator/wordexport2rawyaml.py +257 -0
- pyaota-0.1.0/src/pyaota/generator/yaml2tex.py +368 -0
- pyaota-0.1.0/src/pyaota/grader/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/grader/answersheetreader.py +1308 -0
- pyaota-0.1.0/src/pyaota/grader/autograder.py +226 -0
- pyaota-0.1.0/src/pyaota/latex/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/latex/content.py +210 -0
- pyaota-0.1.0/src/pyaota/latex/latexcompiler.py +90 -0
- pyaota-0.1.0/src/pyaota/ocr/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/ocr/digit_ocr.py +101 -0
- pyaota-0.1.0/src/pyaota/ocr/train_mnist_digit_model.py +63 -0
- pyaota-0.1.0/src/pyaota/util/__init__.py +0 -0
- pyaota-0.1.0/src/pyaota/util/collectors.py +107 -0
- pyaota-0.1.0/src/pyaota/util/command.py +37 -0
- pyaota-0.1.0/src/pyaota/util/text.py +26 -0
pyaota-0.1.0/.gitignore
ADDED
pyaota-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cameron F. Abrams
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyaota-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyaota
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python app for generating and grading exams.
|
|
5
|
+
Author: Cameron F. Abrams
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Cameron F. Abrams
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: docx
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Requires-Dist: opencv-python
|
|
32
|
+
Requires-Dist: pdf2image
|
|
33
|
+
Requires-Dist: pyyaml
|
|
34
|
+
Requires-Dist: tensorflow>=2.12
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: black; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
40
|
+
Provides-Extra: docs
|
|
41
|
+
Requires-Dist: furo; extra == 'docs'
|
|
42
|
+
Requires-Dist: myst-parser; extra == 'docs'
|
|
43
|
+
Requires-Dist: sphinx; extra == 'docs'
|
|
44
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# pyaota
|
|
48
|
+
|
|
49
|
+
> Multiple choice exam question manager, exam generator, and exam grader
|
|
50
|
+
|
pyaota-0.1.0/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyaota"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python app for generating and grading exams."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Cameron F. Abrams" }]
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"pyyaml",
|
|
16
|
+
"pdf2image",
|
|
17
|
+
"numpy",
|
|
18
|
+
"opencv-python",
|
|
19
|
+
"tensorflow>=2.12", # swap for torch if you prefer
|
|
20
|
+
"docx",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest",
|
|
26
|
+
"pytest-cov",
|
|
27
|
+
"ruff",
|
|
28
|
+
"black",
|
|
29
|
+
]
|
|
30
|
+
docs = [
|
|
31
|
+
"sphinx",
|
|
32
|
+
"furo",
|
|
33
|
+
"sphinx-autodoc-typehints",
|
|
34
|
+
"myst-parser",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
pyaota = "pyaota.cli:main"
|
|
39
|
+
|
|
40
|
+
# --- BUILD CONFIG -------------------------------------------------
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/pyaota"]
|
|
44
|
+
|
|
45
|
+
# Include non-.py package data inside src/pyaota/ (e.g., src/pyaota/data/**)
|
|
46
|
+
[tool.hatch.build]
|
|
47
|
+
include = [
|
|
48
|
+
"src/pyaota/data/**",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = [
|
|
53
|
+
"src/pyaota/**",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "-q"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 88
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
|
|
64
|
+
[tool.black]
|
|
65
|
+
line-length = 88
|
|
66
|
+
target-version = ["py310"]
|
|
File without changes
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for pyaota: build and grade multiple-choice exams.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse as ap
|
|
8
|
+
import sys, os, shutil
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from .generator.manager import (
|
|
12
|
+
make_exams_subcommand,
|
|
13
|
+
make_answersheet_subcommand,
|
|
14
|
+
compile_dump_subcommand,
|
|
15
|
+
tune_answersheetreader_subcommand,
|
|
16
|
+
autograde_subcommand
|
|
17
|
+
)
|
|
18
|
+
from .util.text import banner, oxford
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
|
23
|
+
|
|
24
|
+
def setup_logging(args):
|
|
25
|
+
loglevel_numeric = getattr(logging, args.logging_level.upper())
|
|
26
|
+
if args.log:
|
|
27
|
+
if os.path.exists(args.log):
|
|
28
|
+
shutil.copyfile(args.log, args.log+'.bak')
|
|
29
|
+
logging.basicConfig(filename=args.log,
|
|
30
|
+
filemode='w',
|
|
31
|
+
format='%(asctime)s %(name)s %(message)s',
|
|
32
|
+
level=loglevel_numeric
|
|
33
|
+
)
|
|
34
|
+
console = logging.StreamHandler()
|
|
35
|
+
console.setLevel(logging.INFO)
|
|
36
|
+
formatter = logging.Formatter('%(levelname)s> %(message)s')
|
|
37
|
+
console.setFormatter(formatter)
|
|
38
|
+
logging.getLogger('').addHandler(console)
|
|
39
|
+
|
|
40
|
+
def save_args(args, filepath):
|
|
41
|
+
"""
|
|
42
|
+
Save argparse namespace including subcommand to YAML
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
args : argparse.Namespace
|
|
47
|
+
The argparse namespace to save.
|
|
48
|
+
filepath : str
|
|
49
|
+
The path to the YAML file to save the args to.
|
|
50
|
+
"""
|
|
51
|
+
args_dict = vars(args).copy()
|
|
52
|
+
args_dict.pop('func', None)
|
|
53
|
+
args_dict.pop('save_config', None)
|
|
54
|
+
args_dict.pop('config', None)
|
|
55
|
+
with open(filepath, 'w') as f:
|
|
56
|
+
yaml.dump(args_dict, f, default_flow_style=False)
|
|
57
|
+
|
|
58
|
+
def load_args_from_yaml(filepath):
|
|
59
|
+
"""
|
|
60
|
+
Load args from YAML
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
filepath : str
|
|
65
|
+
The path to the YAML file to load the args from.
|
|
66
|
+
"""
|
|
67
|
+
with open(filepath, 'r') as f:
|
|
68
|
+
return yaml.safe_load(f)
|
|
69
|
+
|
|
70
|
+
def main(argv: list[str] | None = None) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Entry point for the pyaota command-line interface.
|
|
73
|
+
"""
|
|
74
|
+
subcommands = {
|
|
75
|
+
'build': dict(
|
|
76
|
+
func = make_exams_subcommand,
|
|
77
|
+
help = 'build documents',
|
|
78
|
+
),
|
|
79
|
+
'compile-dump': dict(
|
|
80
|
+
func = compile_dump_subcommand,
|
|
81
|
+
help = 'compile full dump of questions into a document',
|
|
82
|
+
),
|
|
83
|
+
'grade': dict(
|
|
84
|
+
func = autograde_subcommand,
|
|
85
|
+
help = 'grade exams from scanned answer sheets',
|
|
86
|
+
),
|
|
87
|
+
'make-answersheet': dict(
|
|
88
|
+
func = make_answersheet_subcommand,
|
|
89
|
+
help = 'make a blank answer sheet PDF',
|
|
90
|
+
),
|
|
91
|
+
'tune-answersheetreader': dict(
|
|
92
|
+
func = tune_answersheetreader_subcommand,
|
|
93
|
+
help = 'tune the answer sheet reader parameters',
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
parser = ap.ArgumentParser(
|
|
97
|
+
prog='pyaota',
|
|
98
|
+
description='pyaota: build and grade multiple-choice/true-false exams',
|
|
99
|
+
epilog='(c) 2025-2026 Cameron F. Abrams <cfa22@drexel.edu>'
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument('--config', type=str, help='Load config from YAML')
|
|
102
|
+
parser.add_argument('--save-config', type=str, help='Save full config to YAML')
|
|
103
|
+
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
'-b',
|
|
106
|
+
'--banner',
|
|
107
|
+
default=False,
|
|
108
|
+
action=ap.BooleanOptionalAction,
|
|
109
|
+
help='toggle banner message'
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
'--logging-level',
|
|
113
|
+
type=str,
|
|
114
|
+
default='debug',
|
|
115
|
+
choices=[None, 'info', 'debug', 'warning'],
|
|
116
|
+
help='Logging level for messages written to diagnostic log'
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
'-l',
|
|
120
|
+
'--log',
|
|
121
|
+
type=str,
|
|
122
|
+
default='pyaota-diagnostics.log',
|
|
123
|
+
help='File to which diagnostic log messages are written'
|
|
124
|
+
)
|
|
125
|
+
subparsers = parser.add_subparsers(
|
|
126
|
+
title="subcommands",
|
|
127
|
+
dest="subcommand",
|
|
128
|
+
metavar="<subcommand>",
|
|
129
|
+
required=True,
|
|
130
|
+
)
|
|
131
|
+
command_parsers={}
|
|
132
|
+
for k, specs in subcommands.items():
|
|
133
|
+
command_parsers[k] = subparsers.add_parser(
|
|
134
|
+
k,
|
|
135
|
+
help=specs['help'],
|
|
136
|
+
formatter_class=ap.RawDescriptionHelpFormatter
|
|
137
|
+
)
|
|
138
|
+
# command_parsers[k].set_defaults(func=specs['func'])
|
|
139
|
+
|
|
140
|
+
command_parsers["build"].add_argument(
|
|
141
|
+
"-od",
|
|
142
|
+
"--output-dir",
|
|
143
|
+
help="Output directory",
|
|
144
|
+
)
|
|
145
|
+
command_parsers["build"].add_argument(
|
|
146
|
+
"--institution",
|
|
147
|
+
type=str,
|
|
148
|
+
help="Institution name",
|
|
149
|
+
default="Drexel University"
|
|
150
|
+
)
|
|
151
|
+
command_parsers["build"].add_argument(
|
|
152
|
+
"--course",
|
|
153
|
+
type=str,
|
|
154
|
+
help="Course name",
|
|
155
|
+
default="ENGR-131"
|
|
156
|
+
)
|
|
157
|
+
command_parsers["build"].add_argument(
|
|
158
|
+
"--term",
|
|
159
|
+
type=str,
|
|
160
|
+
help="Term name",
|
|
161
|
+
default="202526"
|
|
162
|
+
)
|
|
163
|
+
command_parsers["build"].add_argument(
|
|
164
|
+
"--cleanup",
|
|
165
|
+
action=ap.BooleanOptionalAction,
|
|
166
|
+
help="Cleanup intermediate files after LaTeX compilation",
|
|
167
|
+
)
|
|
168
|
+
command_parsers["build"].add_argument(
|
|
169
|
+
"--seed",
|
|
170
|
+
type=int,
|
|
171
|
+
help="Random seed for reproducible generation",
|
|
172
|
+
)
|
|
173
|
+
command_parsers["build"].add_argument(
|
|
174
|
+
"-n",
|
|
175
|
+
"--num-exams",
|
|
176
|
+
type=int,
|
|
177
|
+
help="Number of exams to generate",
|
|
178
|
+
)
|
|
179
|
+
command_parsers["build"].add_argument(
|
|
180
|
+
"-t",
|
|
181
|
+
"--topics",
|
|
182
|
+
nargs="+",
|
|
183
|
+
help="Topics to include in the exam (questions will be drawn from these topics and distributed evenly)",
|
|
184
|
+
)
|
|
185
|
+
command_parsers["build"].add_argument(
|
|
186
|
+
"-nq",
|
|
187
|
+
"--num-questions",
|
|
188
|
+
type=int,
|
|
189
|
+
help="Number of questions on each exam",
|
|
190
|
+
)
|
|
191
|
+
command_parsers["build"].add_argument(
|
|
192
|
+
"-q",
|
|
193
|
+
"--question-banks",
|
|
194
|
+
nargs="+",
|
|
195
|
+
help="Paths to question banks (YAML/JSON)",
|
|
196
|
+
)
|
|
197
|
+
command_parsers["build"].add_argument(
|
|
198
|
+
"-nc",
|
|
199
|
+
"--num-cols",
|
|
200
|
+
type=int,
|
|
201
|
+
help="Number of columns in the answer sheet",
|
|
202
|
+
)
|
|
203
|
+
command_parsers["build"].add_argument(
|
|
204
|
+
"-sq",
|
|
205
|
+
"--shuffle-questions",
|
|
206
|
+
default=False,
|
|
207
|
+
action=ap.BooleanOptionalAction,
|
|
208
|
+
help="Shuffle question order on each exam",
|
|
209
|
+
)
|
|
210
|
+
command_parsers["build"].add_argument(
|
|
211
|
+
"-sc",
|
|
212
|
+
"--shuffle-choices",
|
|
213
|
+
default=False,
|
|
214
|
+
action=ap.BooleanOptionalAction,
|
|
215
|
+
help="Shuffle answer choice order on each exam",
|
|
216
|
+
)
|
|
217
|
+
command_parsers["build"].add_argument(
|
|
218
|
+
"-en",
|
|
219
|
+
"--exam-name",
|
|
220
|
+
type=str,
|
|
221
|
+
default="Exam",
|
|
222
|
+
help="Name of the exam (used in header)",
|
|
223
|
+
)
|
|
224
|
+
command_parsers["build"].add_argument(
|
|
225
|
+
"-ow",
|
|
226
|
+
"--overwrite",
|
|
227
|
+
default=False,
|
|
228
|
+
action=ap.BooleanOptionalAction,
|
|
229
|
+
help="Overwrite output directory if it exists",
|
|
230
|
+
)
|
|
231
|
+
command_parsers["compile-dump"].add_argument(
|
|
232
|
+
"-od",
|
|
233
|
+
"--output-dir",
|
|
234
|
+
default=".",
|
|
235
|
+
help="Output directory",
|
|
236
|
+
)
|
|
237
|
+
command_parsers["compile-dump"].add_argument(
|
|
238
|
+
"-q",
|
|
239
|
+
"--question-banks",
|
|
240
|
+
nargs="+",
|
|
241
|
+
default=[],
|
|
242
|
+
help="Paths to question banks (YAML/JSON)",
|
|
243
|
+
)
|
|
244
|
+
command_parsers["compile-dump"].add_argument(
|
|
245
|
+
"--institution",
|
|
246
|
+
type=str,
|
|
247
|
+
help="Institution name",
|
|
248
|
+
default="Drexel University"
|
|
249
|
+
)
|
|
250
|
+
command_parsers["compile-dump"].add_argument(
|
|
251
|
+
"--course",
|
|
252
|
+
type=str,
|
|
253
|
+
help="Course name",
|
|
254
|
+
default="ENGR-131"
|
|
255
|
+
)
|
|
256
|
+
command_parsers["compile-dump"].add_argument(
|
|
257
|
+
"--term",
|
|
258
|
+
type=str,
|
|
259
|
+
help="Term name",
|
|
260
|
+
default="202526"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
command_parsers["grade"].add_argument(
|
|
264
|
+
"-i",
|
|
265
|
+
"--input-pdf",
|
|
266
|
+
help="PDF containing one or more answer sheets (scantron-like)",
|
|
267
|
+
)
|
|
268
|
+
command_parsers["grade"].add_argument(
|
|
269
|
+
# can handle multiple files
|
|
270
|
+
"-k",
|
|
271
|
+
"--keyfiles",
|
|
272
|
+
nargs="+",
|
|
273
|
+
help="CSV file(s) containing answer keys for each exam version",
|
|
274
|
+
)
|
|
275
|
+
command_parsers["grade"].add_argument(
|
|
276
|
+
"-aly",
|
|
277
|
+
"--answersheet-layout-yaml",
|
|
278
|
+
help="YAML file specifying the answer sheet layout configuration",
|
|
279
|
+
)
|
|
280
|
+
command_parsers["grade"].add_argument(
|
|
281
|
+
"-od",
|
|
282
|
+
"--output-dir",
|
|
283
|
+
default=".",
|
|
284
|
+
help="Path to output directory for graded results",
|
|
285
|
+
)
|
|
286
|
+
command_parsers["grade"].add_argument(
|
|
287
|
+
"--debug-output-dir",
|
|
288
|
+
default = "debug-autograder",
|
|
289
|
+
help="Path to output directory for debug images",
|
|
290
|
+
)
|
|
291
|
+
command_parsers["grade"].add_argument(
|
|
292
|
+
"-og",
|
|
293
|
+
"--gradesheet-output-csv",
|
|
294
|
+
default="graded_results.csv",
|
|
295
|
+
help="Path to output CSV file summarizing results",
|
|
296
|
+
)
|
|
297
|
+
command_parsers["grade"].add_argument(
|
|
298
|
+
"-oq",
|
|
299
|
+
"--question-tally-output-csv",
|
|
300
|
+
default="question_tally.csv",
|
|
301
|
+
help="Path to output CSV file summarizing question tallies",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
command_parsers["make-answersheet"].add_argument(
|
|
305
|
+
"-o",
|
|
306
|
+
"--output-pdf",
|
|
307
|
+
default="answersheet",
|
|
308
|
+
help="Path to output answer sheet PDF",
|
|
309
|
+
)
|
|
310
|
+
command_parsers["make-answersheet"].add_argument(
|
|
311
|
+
"-nc",
|
|
312
|
+
"--num-cols",
|
|
313
|
+
type=int,
|
|
314
|
+
default=3,
|
|
315
|
+
help="Number of columns in the answer sheet",
|
|
316
|
+
)
|
|
317
|
+
command_parsers["make-answersheet"].add_argument(
|
|
318
|
+
"-nq",
|
|
319
|
+
"--num-questions",
|
|
320
|
+
type=int,
|
|
321
|
+
default=50,
|
|
322
|
+
help="Number of questions on the answer sheet",
|
|
323
|
+
)
|
|
324
|
+
command_parsers["make-answersheet"].add_argument(
|
|
325
|
+
"-od",
|
|
326
|
+
"--output-dir",
|
|
327
|
+
default=".",
|
|
328
|
+
help="Output directory",
|
|
329
|
+
)
|
|
330
|
+
command_parsers["make-answersheet"].add_argument(
|
|
331
|
+
"-idl",
|
|
332
|
+
"--student-id-num-digits",
|
|
333
|
+
type=int,
|
|
334
|
+
default=8,
|
|
335
|
+
help="Number of digits in the student ID field",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
command_parsers["tune-answersheetreader"].add_argument(
|
|
339
|
+
"-i",
|
|
340
|
+
"--sample-pdf",
|
|
341
|
+
help="Sample PDF for tuning results",
|
|
342
|
+
)
|
|
343
|
+
command_parsers["tune-answersheetreader"].add_argument(
|
|
344
|
+
"-nc",
|
|
345
|
+
"--num-cols",
|
|
346
|
+
type=int,
|
|
347
|
+
default=3,
|
|
348
|
+
help="Number of columns in the answer sheet",
|
|
349
|
+
)
|
|
350
|
+
command_parsers["tune-answersheetreader"].add_argument(
|
|
351
|
+
"-nq",
|
|
352
|
+
"--num-questions",
|
|
353
|
+
type=int,
|
|
354
|
+
default=50,
|
|
355
|
+
help="Number of questions on the answer sheet",
|
|
356
|
+
)
|
|
357
|
+
command_parsers["tune-answersheetreader"].add_argument(
|
|
358
|
+
"-o",
|
|
359
|
+
"--output-image",
|
|
360
|
+
default="answersheetreader_tuning_overlay.png",
|
|
361
|
+
help="Path to output image file showing tuning overlay",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# ---- dispatch ------------------------------------------------
|
|
365
|
+
args = parser.parse_args()
|
|
366
|
+
|
|
367
|
+
# If config specified, load and override
|
|
368
|
+
if args.config:
|
|
369
|
+
config_dict = load_args_from_yaml(args.config)
|
|
370
|
+
logger.debug(f'Loaded config from {args.config}')
|
|
371
|
+
logger.debug(f'Config contents: {config_dict}')
|
|
372
|
+
# Only override if not set on command line
|
|
373
|
+
for key, value in config_dict.items():
|
|
374
|
+
if not hasattr(args, key) or getattr(args, key) is None or isinstance(getattr(args, key), bool):
|
|
375
|
+
setattr(args, key, value)
|
|
376
|
+
logger.debug(f'Args after loading config: {args}')
|
|
377
|
+
|
|
378
|
+
# Save if requested
|
|
379
|
+
if args.save_config:
|
|
380
|
+
save_args(args, args.save_config)
|
|
381
|
+
|
|
382
|
+
setup_logging(args)
|
|
383
|
+
|
|
384
|
+
if args.banner:
|
|
385
|
+
banner(print)
|
|
386
|
+
func = subcommands.get(args.subcommand, {}).get('func', None)
|
|
387
|
+
if func:
|
|
388
|
+
return func(args)
|
|
389
|
+
else:
|
|
390
|
+
logger.debug(f'{args}')
|
|
391
|
+
my_list = oxford(list(subcommands.keys()))
|
|
392
|
+
print(f'No subcommand found. Expected one of {my_list}')
|
|
393
|
+
return 1
|
|
394
|
+
logger.info('Thanks for using pyaota!')
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
raise SystemExit(main())
|
|
Binary file
|
|
File without changes
|