Source code for pygacity.cli

# Author: Cameron F. Abrams, <cfa22@drexel.edu>

import logging
import os
import shutil

import argparse as ap

from .generate.build import build, latex_subcommand
from .util.distribute import distribute_subcommand
from .util.pdfutils import bundle_subcommand
from .util.stringthings import oxford, banner

logger = logging.getLogger(__name__)

[docs] def setup_logging(args): loglevel_numeric = getattr(logging, args.logging_level.upper()) if args.log: if os.path.exists(args.log): shutil.copyfile(args.log, args.log+'.bak') logging.basicConfig(filename=args.log, filemode='w', format='%(asctime)s %(name)s %(message)s', level=loglevel_numeric ) console = logging.StreamHandler() console.setLevel(logging.INFO) formatter = logging.Formatter('%(levelname)s> %(message)s') console.setFormatter(formatter) logging.getLogger('').addHandler(console)
[docs] def cli(): subcommands = { 'build': dict( func = build, help = 'build document', ), 'singlet': dict( func = build, help = 'build a single problem' ), 'bundle': dict( func = bundle_subcommand, help = 'bundle student exam PDFs from a build directory into print-ready PDFs', ), 'distribute': dict( func = distribute_subcommand, help = 'distribute built exam PDFs to students', ), 'latex': dict( func = latex_subcommand, help = 'compile an arbitrary LaTeX file with autoprob.cls on the search path', ), } parser = ap.ArgumentParser( prog='pygacity', description='pygacity -- build customized engineering assignment/exam documents', epilog='(c) 2025 Cameron F. Abrams <cfa22@drexel.edu>' ) parser.add_argument( '-b', '--banner', default=False, action=ap.BooleanOptionalAction, help='toggle banner message' ) parser.add_argument( '--logging-level', type=str, default='debug', choices=[None, 'info', 'debug', 'warning'], help='Logging level for messages written to diagnostic log' ) parser.add_argument( '-l', '--log', type=str, default='pygacity-diagnostics.log', help='File to which diagnostic log messages are written' ) subparsers = parser.add_subparsers( title="subcommands", dest="command", metavar="<command>", required=True, ) command_parsers={} for k, specs in subcommands.items(): command_parsers[k] = subparsers.add_parser( k, help=specs['help'], formatter_class=ap.RawDescriptionHelpFormatter ) command_parsers[k].set_defaults(func=specs['func']) command_parsers['build'].add_argument( '-o', '--overwrite', type=bool, default=False, action=ap.BooleanOptionalAction, help='completely remove old save dir and build new exams') command_parsers['build'].add_argument( '--bundle-size', type=int, default=None, dest='bundle_size', metavar='N', help='number of exams per printed bundle PDF (default: 10; 0 disables bundling; overrides YAML build.bundle-size)') command_parsers['build'].add_argument( '--two-sided', default=None, action=ap.BooleanOptionalAction, dest='two_sided', help='pad each exam to an even page count so the next exam starts on a right-hand page (overrides YAML build.two-sided)') command_parsers['build'].add_argument( 'f', help='mandatory YAML input file') command_parsers['bundle'].add_argument( 'build_dir', help='directory containing built student exam PDFs') command_parsers['bundle'].add_argument( '--bundle-size', type=int, default=10, dest='bundle_size', metavar='N', help='number of exams per bundle PDF (default: 10)') command_parsers['bundle'].add_argument( '--two-sided', default=False, action=ap.BooleanOptionalAction, dest='two_sided', help='pad each exam to an even page count so the next exam starts on a right-hand page') command_parsers['singlet'].add_argument( 'texfile', type=str, help='tex file containing single problem' ) command_parsers['singlet'].add_argument( '-o', '--overwrite', type=bool, default=True, action=ap.BooleanOptionalAction, help='completely remove old save dir and build new exams') command_parsers['latex'].add_argument( 'texfile', type=str, help='LaTeX source file to compile' ) command_parsers['latex'].add_argument( '-o', '--output-dir', type=str, default='.', help='directory for output files (default: current directory)' ) command_parsers['latex'].add_argument( '-n', '--runs', type=int, default=2, help='number of latexmk invocations when no pycode blocks are present (default: 2)' ) command_parsers['distribute'].add_argument( 'build_dir', help='directory containing built exam PDFs') command_parsers['distribute'].add_argument( 'gradebook', help='path to Blackboard Learn gradebook CSV') command_parsers['distribute'].add_argument( '-o', '--output-dir', type=str, default='distributed', help='root directory for per-student subfolders (default: distributed/)') command_parsers['distribute'].add_argument( '-fc', '--filter-column', type=str, default=None, help='gradebook column where "yes" or "true" selects students to receive exams') command_parsers['distribute'].add_argument( '--email', default=False, action=ap.BooleanOptionalAction, help='send exams to students via Outlook') command_parsers['distribute'].add_argument( '--email-suffix', type=str, default='@drexel.edu', help='suffix appended to Username for email address (default: @drexel.edu)') command_parsers['distribute'].add_argument( '--subject', type=str, default='Your exam', help='email subject line') command_parsers['distribute'].add_argument( '--body', type=str, default='Attached is your exam. Please contact your instructor with any questions.', help='plain-text email body') command_parsers['distribute'].add_argument( '--dry-run', default=False, action=ap.BooleanOptionalAction, help='preview email actions without actually sending') command_parsers['distribute'].add_argument( '--include-solutions', default=True, action=ap.BooleanOptionalAction, help='include solution PDFs in student folders and emails (default: yes; use --no-include-solutions to omit)') args = parser.parse_args() setup_logging(args) if args.banner: banner(print) if hasattr(args, 'func'): args.func(args) else: my_list = oxford(list(subcommands.keys())) print(f'No subcommand found. Expected one of {my_list}') logger.info('Thanks for using pygacity!')