#!/usr/bin/env python3 import argparse import copy import datetime import sys import yaml import TeachingCalendar class PeriodError(KeyError): pass class PaperError(KeyError): pass class LectureStyleError(KeyError): pass def parse_command_line(): """Parse command-line arguments. Example command lines: generate_dates --style calendar --format latex --year 2020 --period S1 --paper INFO201 generate_dates --style lecture --format xml --period S2 --paper INFO202 """ format_map = { "t": "text", "text": "text", "l": "latex", "latex": "latex", "x": "xml", "xml": "xml", } style_map = { "c": "calendar", "calendar": "calendar", "l": "lecture", "lecture": "lecture", "i": "iso", "iso": "iso", } parser = argparse.ArgumentParser( prog="generate_dates", description="") parser.add_argument( "--config", "-c", default="config.yml", help="file name of the configuration file " "[default 'config.yml']") parser.add_argument( "--debug", "-d", action='store_true', help="enable debugging output") parser.add_argument( "--format", "-f", default="latex", choices=["text", "t", "latex", "l", "xml", "x"], help="output format [default 'latex']") parser.add_argument( "--output", "-o", type=argparse.FileType("w", encoding="UTF-8"), help="file name to write the output to; existing " "files of the same name will be overwritten!") parser.add_argument( "--paper", "-p", default=None, help="paper code (e.g., INFO201) [required for 'lecture' style, " "ignored otherwise]") parser.add_argument( "--style", "-s", default="calendar", choices=["calendar", "c", "lecture", "l", "iso", "i"], help="output style: 'calendar' for teaching calendars in course " "outlines, 'lecture' for individual lecture dates, or 'iso'" "for dates across the entire ISO-8601 year [default 'calendar']") parser.add_argument( "--teaching-period", "-t", default=None, choices=["SS", "S1", "S2", "FY"], dest="period", help="teaching period [required]") parser.add_argument( "--year", "-y", type=int, default=datetime.date.today().year, help=("the year to generate dates for [default {y}]".format( y=datetime.date.today().year))) args = parser.parse_args() # normalise format and style args.format = format_map[args.format] args.style = style_map[args.style] if args.style == "calendar": # --paper is irrelevant if args.paper: print("generate_dates: warning: --paper/-p is ignored for " "'calendar' style") # --teaching-period is required if not args.period: parser.exit(2, "generate_dates: error: --teaching-period/-t " "is required for 'calendar' style\n") elif args.style == "iso": # both --paper and --teaching-period are irrelevant if args.paper or args.period: print("generate_dates: warning: --paper/-p and " "--teaching-period/-t are ignored for 'iso' style") elif args.style == "lecture": # both --paper and --teaching-period are required if not args.paper: parser.exit(2, message="generate_dates: error: -paper/-p and " "--teaching-period/-t are required for 'lecture' style\n") # --teaching-period must be specified if --style is "calendar" or "lecture" else: pass return args def parse_config(config, args): """Validate and filter the top-level configuration. Returns separate references to the period configuration and the paper configuration, as applicable. (Yes, the period configuration includes the paper configuration, but it may also include other papers, so it's more convenient to return a separate reference for the specified paper.) The configuration file will normally contain specifications for several teaching periods and papers. We don't need all of this: usually we're only interested in one teaching period and one paper (if relevant). """ period_config = paper_config = None # this is all irrelevant if we're going ISO style if args.style != "iso": if args.period not in config.keys(): raise PeriodError() period_config = config[args.period] # raises KeyError if source key is missing period_config["name"] and period_config["weeks"] # papers are irrelevant in "calendar" style if args.style == "lecture": if "papers" in period_config.keys(): if args.paper in period_config["papers"].keys(): paper_config = period_config["papers"][args.paper] # we expect at least a "lectures" entry paper_config["lectures"] else: raise PaperError() else: raise LectureStyleError() return period_config, paper_config def main(): args = parse_command_line() if args.debug: print("DEBUG: args: {0}".format(args)) config = yaml.safe_load(open(args.config)) if args.debug: print("DEBUG: config: {0}".format(config)) try: period_config, paper_config = parse_config(config, args) if args.debug: print("DEBUG: period config: {0}".format(period_config)) print("DEBUG: paper config: {0}".format(paper_config)) # priority: year on command line (defaults to current year), # year in config if period_config: args.year = ( config["year"] if "year" in config.keys() else args.year) cal = TeachingCalendar.TeachingCalendar( args.year, args.period, args.paper, period_config, paper_config) print("Period = {p}".format(p=args.period)) print(cal.calendar()) print(cal.lecture_dates()) except LectureStyleError as e: print("ERROR: 'lecture' style was requested but {c} contains no " "papers for {t}.".format(c=args.config, t=args.period)) except PaperError as e: print("ERROR: no entry in {c} for paper {p} ({t}).".format( c=args.config, p=args.paper, t=args.period)) except PeriodError as e: print("ERROR: no entry in {c} for teaching period " "{p}.".format(c=args.config, p=args.period)) except KeyError as e: print("ERROR: teaching period {p} in {c} is missing required " "key {k}.".format(p=args.period, c=args.config, k=e)) if __name__ == "__main__": main()