GitBucket
4.21.2
Toggle navigation
Snippets
Sign in
Files
Branches
1
Releases
Issues
1
Pull requests
Labels
Priorities
Milestones
Wiki
Forks
nigel.stanger
/
Handbook
Browse code
Eliminated silly “config.config”
master
1 parent
53d2459
commit
5bc4ea04b5876b206521bc47ad07296b2b2a5d19
Nigel Stanger
authored
on 3 Feb 2021
Patch
Showing
2 changed files
calendar/teachingdates/calendars/basecalendar.py
calendar/teachingdates/config/config.py
Ignore Space
Show notes
View
calendar/teachingdates/calendars/basecalendar.py
import calendar import datetime import jinja2 from num2words import num2words from teachingdates import PROG from .errors import TemplateStyleError from .weeks import BreakWeek, IsoWeek, TeachingWeek suffix_map = { "latex": "tex", "text": "txt", "xml": "xml", } filters = { "isteaching": lambda w: isinstance(w, TeachingWeek), "isbreak": lambda w: isinstance(w, BreakWeek), "pad": lambda s, n: str(s).rjust(n), "num2words": lambda s: num2words(str(s)).title().replace("-", "") } class TeachingCalendar(): """This class generates teaching-related dates for a specific paper offered in a specific teaching period of a specific year. If you don't provide teaching period details, it generates dates for the current ISO-8601 year. """ weeks = [] lecture_offsets = [] skipped_lectures = [] period = None period_name = None paper = None end_of_week = None year = datetime.date.today().year def __init__(self, config): self.config = config cal = calendar.Calendar().yeardatescalendar(config.year, width=12) # Extract the Mondays at the start of each week. Because Calendar # is designed to produce data suitable for a printed calendar, # it includes all weeks that overlap with the calendar year, # regardless of whether they're actually part of the ISO year. # We therefore also need to filter on the current ISO year. self.mondays = [ week[0] for month in cal[0] for week in month if week[0].isocalendar()[0] == config.year ] # Similarly, each month generated by Calendar includes all days # from the first and last weeks of the month, which may be from # the preceding or succeeding month, respectively. These show # up as duplicates that need to be removed. self.mondays = list(dict.fromkeys(self.mondays)) self.period_weeks = {} self.calendars = {} for period in self.config.get_config_keys(): self.period_weeks[period] = self.generate_period_weeks( self.config.get_period_config(period)["weeks"]) self.calendars[period] = self.generate_calendar(period) self.period_weeks["iso"] = self.generate_period_weeks( [{"iso": [ self.mondays[0].isocalendar()[1], # first ISO week self.mondays[-1].isocalendar()[1] # last ISO week ]}]) self.calendars["iso"] = self.generate_calendar("iso") # print("mondays (__init__): ", self.mondays) # print("period_weeks (__init__): ", self.period_weeks) # print("calendars (__init__): ", self.calendars) # def __init__(self, # year, # period, # paper, # period_name="ISO", # end_of_week=None, # period_config=None, # paper_config=None, # config=None): # self.year = year # self.period = period # self.period_name = period_name.replace(" ", "") # self.paper = paper # self.end_of_week = end_of_week # self.config = config # # self.env.filters.update(filters) # # This is likely to end up with one week too many at the end, # # but that doesn't matter as there will never be any teaching # # during the last week of December! # cal = calendar.Calendar().yeardatescalendar(self.year, width=12) # self.mondays = [week[0] for month in cal[0] for week in month] # self.mondays = list(dict.fromkeys(self.mondays)) # if period_config: # self.update_weeks(period_config["weeks"]) # else: # last_week = datetime.date(self.year, 12, 28).isocalendar()[1] # self.update_weeks([{"iso": [1, last_week]}]) # if paper_config: # self.update_lectures(paper_config["lectures"]) def make_week(self, week_type, start, end=None): end = start if end is None else end return [ week_type( self.mondays[w].year, self.mondays[w].month, self.mondays[w].day) for w in range(start - 1, end)] # def teaching_date(self, period, week, offset=0): # pass # def update_weeks(self, week_list): # for w in week_list: # for t, r in w.items(): # if t == "teach": # self.weeks += self.make_week(TeachingWeek, r[0], r[1]) # elif t == "break": # self.weeks += self.make_week(BreakWeek, r[0], r[1]) # elif t == "iso": # self.weeks += self.make_week(IsoWeek, r[0], r[1]) # else: # print("{prog}: warning: ignored unknown week type " # "'{type}'.".format(prog=PROG, type=t)) def generate_period_weeks(self, week_list): weeks = [] for w in week_list: for t, r in w.items(): if t == "teach": weeks += self.make_week(TeachingWeek, r[0], r[1]) elif t == "break": weeks += self.make_week(BreakWeek, r[0], r[1]) elif t == "iso": weeks += self.make_week(IsoWeek, r[0], r[1]) else: print("{prog}: warning: ignored unknown week type " "'{type}'.".format(prog=PROG, type=t)) return weeks def update_lectures(self, lecture_list): for l in lecture_list: for t, v in l.items(): if t == "offsets": self.lecture_offsets = v elif t == "skip": self.skipped_lectures = v else: print("{prog}: warning: ignored unknown lecture key " "'{key}'.".format(prog=PROG, key=t)) # turn this into a generator? def generate_calendar(self, period): result = {} week_num = 0 break_num = 0 for w in self.period_weeks[period]: if isinstance(w, (TeachingWeek, IsoWeek)): # Increment first so that break weeks have the # same week number as the preceding teaching week. # This ensures the keys are always chronologically # sorted. Reset the break number for each new # teaching week. week_num += 1 break_num = 0 result[week_num] = w # if period == "ISO": # print("week ", week_num, " ", w) elif isinstance(w, BreakWeek): # Allow for up to 99 consecutive break weeks, # which should be sufficient :). Should probably # throw an exception if we exceed that. break_num += 0.01 result[week_num + break_num] = w return result # turn this into a generator? def lecture_dates(self): dates = {} lecture_num = 1 teaching_weeks = [t for t in self.weeks if isinstance(t, TeachingWeek)] for week_index, monday in enumerate(teaching_weeks): for offset_index, offset in enumerate(self.lecture_offsets): lec = week_index * 2 + offset_index + 1 if lec in self.skipped_lectures: continue dates[lecture_num] = monday + datetime.timedelta(offset) lecture_num += 1 return dates def render_latex(self): env = jinja2.Environment( block_start_string="\\BLOCK{", block_end_string="}", variable_start_string="\\VAR{", variable_end_string="}", comment_start_string="\\#{", comment_end_string="}", line_statement_prefix="%%", line_comment_prefix="%#", trim_blocks=True, autoescape=False, loader=jinja2.PackageLoader("teachingdates", "templates")) env.filters.update(filters) def render_text(self): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=False) env.filters.update(filters) template = env.get_template( "{style}.txt.j2".format(style=self.config.style)) if self.config.style == "lecture": return template.render( weeks=self.lecture_dates(), paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) else: return template.render( weeks=self.calendars[self.config.period], paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) def render_xml(self): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=jinja2.select_autoescape(["xml"])) env.filters.update(filters) template = env.get_template("paper-calendar-dates.xml.j2") return template.render( calendars=self.calendars, eow_offset=datetime.timedelta(self.config.end_of_week)) def render(self, style, fmt): if fmt == "latex": return(self.render_latex()) elif fmt == "text": return(self.render_text()) elif fmt == "xml": return(self.render_xml()) else: return None
import calendar import datetime import jinja2 from num2words import num2words from teachingdates import PROG from .errors import TemplateStyleError from .weeks import BreakWeek, IsoWeek, TeachingWeek suffix_map = { "latex": "tex", "text": "txt", "xml": "xml", } filters = { "isteaching": lambda w: isinstance(w, TeachingWeek), "isbreak": lambda w: isinstance(w, BreakWeek), "pad": lambda s, n: str(s).rjust(n), "num2words": lambda s: num2words(str(s)).title().replace("-", "") } class TeachingCalendar(): """This class generates teaching-related dates for a specific paper offered in a specific teaching period of a specific year. If you don't provide teaching period details, it generates dates for the current ISO-8601 year. """ weeks = [] lecture_offsets = [] skipped_lectures = [] period = None period_name = None paper = None end_of_week = None year = datetime.date.today().year def __init__(self, config): self.config = config cal = calendar.Calendar().yeardatescalendar(config.year, width=12) # Extract the Mondays at the start of each week. Because Calendar # is designed to produce data suitable for a printed calendar, # it includes all weeks that overlap with the calendar year, # regardless of whether they're actually part of the ISO year. # We therefore also need to filter on the current ISO year. self.mondays = [ week[0] for month in cal[0] for week in month if week[0].isocalendar()[0] == config.year ] # Similarly, each month generated by Calendar includes all days # from the first and last weeks of the month, which may be from # the preceding or succeeding month, respectively. These show # up as duplicates that need to be removed. self.mondays = list(dict.fromkeys(self.mondays)) self.period_weeks = {} self.calendars = {} for period in self.config.config.keys(): self.period_weeks[period] = self.generate_period_weeks( self.config.config[period]["weeks"]) self.calendars[period] = self.generate_calendar(period) self.period_weeks["iso"] = self.generate_period_weeks( [{"iso": [ self.mondays[0].isocalendar()[1], # first ISO week self.mondays[-1].isocalendar()[1] # last ISO week ]}]) self.calendars["iso"] = self.generate_calendar("iso") # print("mondays (__init__): ", self.mondays) # print("period_weeks (__init__): ", self.period_weeks) # print("calendars (__init__): ", self.calendars) # def __init__(self, # year, # period, # paper, # period_name="ISO", # end_of_week=None, # period_config=None, # paper_config=None, # config=None): # self.year = year # self.period = period # self.period_name = period_name.replace(" ", "") # self.paper = paper # self.end_of_week = end_of_week # self.config = config # # self.env.filters.update(filters) # # This is likely to end up with one week too many at the end, # # but that doesn't matter as there will never be any teaching # # during the last week of December! # cal = calendar.Calendar().yeardatescalendar(self.year, width=12) # self.mondays = [week[0] for month in cal[0] for week in month] # self.mondays = list(dict.fromkeys(self.mondays)) # if period_config: # self.update_weeks(period_config["weeks"]) # else: # last_week = datetime.date(self.year, 12, 28).isocalendar()[1] # self.update_weeks([{"iso": [1, last_week]}]) # if paper_config: # self.update_lectures(paper_config["lectures"]) def make_week(self, week_type, start, end=None): end = start if end is None else end return [ week_type( self.mondays[w].year, self.mondays[w].month, self.mondays[w].day) for w in range(start - 1, end)] # def teaching_date(self, period, week, offset=0): # pass # def update_weeks(self, week_list): # for w in week_list: # for t, r in w.items(): # if t == "teach": # self.weeks += self.make_week(TeachingWeek, r[0], r[1]) # elif t == "break": # self.weeks += self.make_week(BreakWeek, r[0], r[1]) # elif t == "iso": # self.weeks += self.make_week(IsoWeek, r[0], r[1]) # else: # print("{prog}: warning: ignored unknown week type " # "'{type}'.".format(prog=PROG, type=t)) def generate_period_weeks(self, week_list): weeks = [] for w in week_list: for t, r in w.items(): if t == "teach": weeks += self.make_week(TeachingWeek, r[0], r[1]) elif t == "break": weeks += self.make_week(BreakWeek, r[0], r[1]) elif t == "iso": weeks += self.make_week(IsoWeek, r[0], r[1]) else: print("{prog}: warning: ignored unknown week type " "'{type}'.".format(prog=PROG, type=t)) return weeks def update_lectures(self, lecture_list): for l in lecture_list: for t, v in l.items(): if t == "offsets": self.lecture_offsets = v elif t == "skip": self.skipped_lectures = v else: print("{prog}: warning: ignored unknown lecture key " "'{key}'.".format(prog=PROG, key=t)) # turn this into a generator? def generate_calendar(self, period): result = {} week_num = 0 break_num = 0 for w in self.period_weeks[period]: if isinstance(w, (TeachingWeek, IsoWeek)): # Increment first so that break weeks have the # same week number as the preceding teaching week. # This ensures the keys are always chronologically # sorted. Reset the break number for each new # teaching week. week_num += 1 break_num = 0 result[week_num] = w # if period == "ISO": # print("week ", week_num, " ", w) elif isinstance(w, BreakWeek): # Allow for up to 99 consecutive break weeks, # which should be sufficient :). Should probably # throw an exception if we exceed that. break_num += 0.01 result[week_num + break_num] = w return result # turn this into a generator? def lecture_dates(self): dates = {} lecture_num = 1 teaching_weeks = [t for t in self.weeks if isinstance(t, TeachingWeek)] for week_index, monday in enumerate(teaching_weeks): for offset_index, offset in enumerate(self.lecture_offsets): lec = week_index * 2 + offset_index + 1 if lec in self.skipped_lectures: continue dates[lecture_num] = monday + datetime.timedelta(offset) lecture_num += 1 return dates def render_latex(self): env = jinja2.Environment( block_start_string="\\BLOCK{", block_end_string="}", variable_start_string="\\VAR{", variable_end_string="}", comment_start_string="\\#{", comment_end_string="}", line_statement_prefix="%%", line_comment_prefix="%#", trim_blocks=True, autoescape=False, loader=jinja2.PackageLoader("teachingdates", "templates")) env.filters.update(filters) def render_text(self): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=False) env.filters.update(filters) template = env.get_template( "{style}.txt.j2".format(style=self.config.style)) if self.config.style == "lecture": return template.render( weeks=self.lecture_dates(), paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) else: return template.render( weeks=self.calendars[self.config.period], paper=self.config.paper, period=self.config.period, year=self.config.year, eow_offset=datetime.timedelta(self.config.end_of_week)) def render_xml(self): env = jinja2.Environment( loader=jinja2.PackageLoader("teachingdates", "templates"), autoescape=jinja2.select_autoescape(["xml"])) env.filters.update(filters) template = env.get_template("paper-calendar-dates.xml.j2") return template.render( calendars=self.calendars, eow_offset=datetime.timedelta(self.config.end_of_week)) def render(self, style, fmt): if fmt == "latex": return(self.render_latex()) elif fmt == "text": return(self.render_text()) elif fmt == "xml": return(self.render_xml()) else: return None
Ignore Space
Show notes
View
calendar/teachingdates/config/config.py
import datetime import importlib.resources as pkg_resources from pathlib import Path from shutil import copyfile import yaml from teachingdates import PROG from .errors import (ConfigurationError, PaperError, PaperKeyError, PeriodError, PeriodKeyError) CONFIG_FILENAME = "config.yml" CONFIG_DIR = Path.home().joinpath(".config", PROG) CONFIG_FILE = CONFIG_DIR.joinpath(CONFIG_FILENAME) class Configuration(object): """Validate and structure the configuration for easy access. Includes methods to return 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 these separately.) 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). """ style = None period = None period_name = "ISO" paper = None year = None end_of_week = None config = None config_file = None def __init__(self, args): self.load_config(file=args.config_file) self.style = args.style self.period = args.period self.paper = args.paper self.end_of_week = args.end_of_week # priority: year on command line (defaults to current year), # year in config self.year = (self.config["year"] if "year" in self.config.keys() else args.year) period_config = paper_config = None if self.style != "iso": if self.period not in self.config.keys(): raise PeriodError(str(self.config_file)) period_config = self.config[self.period] # the "name" and "weeks" keys are required for key in ["name", "weeks"]: if key not in period_config.keys(): raise PeriodKeyError(key, self.period, self.config_file) self.period_name = period_config["name"] # Papers are only relevant in "lecture" style, which by definition # must specify a paper. try: if self.style == "lecture": if ("papers" in period_config.keys() and self.paper in period_config["papers"].keys()): paper_config = period_config["papers"][self.paper] # we expect at least a "lectures" entry if (not paper_config or "lectures" not in paper_config.keys()): raise PaperKeyError("lectures", self.paper, self.config_file) else: raise PaperError(str(self.config_file)) except AttributeError as e: raise PaperError(str(self.config_file)) def load_config(self, file=None): """Load a configuration file. If no file is specified, try the default configuration in ~/.config. If that doesn't exist, copy the default configuration from teachingdates.config. """ cfg = None if not file: file = CONFIG_FILE if not CONFIG_FILE.exists(): CONFIG_DIR.mkdir(exist_ok=True) configmgr = pkg_resources.path("teachingdates.config", CONFIG_FILENAME) with configmgr as f: copyfile(f, CONFIG_FILE) print("{prog}: created new configuration file at " "'{f}'".format(prog=PROG, f=str(CONFIG_FILE))) try: with open(file) as f: cfg = yaml.safe_load(f.read()) except FileNotFoundError as e: print("{prog}: error: no such file '{file}'".format(prog=PROG, file=file)) if not cfg: raise ConfigurationError(str(file)) self.config_file = file self.config = cfg def get_period_config(self, period=None): """Get the configuration details for a teaching period. No need to specify the year as all configurations are for a single year anyway. """ period = self.period if period is None else period if period in self.config.keys(): return self.config[period] else: return None def get_paper_config(self, period=None, paper=None): """Get the configuration details for a paper. Can optionally specify the teaching period. No need to specify the year as all configurations are for a single year anyway. """ paper = self.paper if paper is None else paper period_config = self.get_period_config(period=period) if (period_config and "papers" in period_config.keys() and paper in period_config["papers"].keys()): return period_config["papers"][paper] else: return None def get_config_keys(self): return self.config.keys()
import datetime import importlib.resources as pkg_resources from pathlib import Path from shutil import copyfile import yaml from teachingdates import PROG from .errors import (ConfigurationError, PaperError, PaperKeyError, PeriodError, PeriodKeyError) CONFIG_FILENAME = "config.yml" CONFIG_DIR = Path.home().joinpath(".config", PROG) CONFIG_FILE = CONFIG_DIR.joinpath(CONFIG_FILENAME) class Configuration(object): """Validate and structure the configuration for easy access. Includes methods to return 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 these separately.) 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). """ style = None period = None period_name = "ISO" paper = None year = None end_of_week = None config = None config_file = None def __init__(self, args): self.load_config(file=args.config_file) self.style = args.style self.period = args.period self.paper = args.paper self.end_of_week = args.end_of_week # priority: year on command line (defaults to current year), # year in config self.year = (self.config["year"] if "year" in self.config.keys() else args.year) period_config = paper_config = None if self.style != "iso": if self.period not in self.config.keys(): raise PeriodError(str(self.config_file)) period_config = self.config[self.period] # the "name" and "weeks" keys are required for key in ["name", "weeks"]: if key not in period_config.keys(): raise PeriodKeyError(key, self.period, self.config_file) self.period_name = period_config["name"] # Papers are only relevant in "lecture" style, which by definition # must specify a paper. try: if self.style == "lecture": if ("papers" in period_config.keys() and self.paper in period_config["papers"].keys()): paper_config = period_config["papers"][self.paper] # we expect at least a "lectures" entry if (not paper_config or "lectures" not in paper_config.keys()): raise PaperKeyError("lectures", self.paper, self.config_file) else: raise PaperError(str(self.config_file)) except AttributeError as e: raise PaperError(str(self.config_file)) def load_config(self, file=None): """Load a configuration file. If no file is specified, try the default configuration in ~/.config. If that doesn't exist, copy the default configuration from teachingdates.config. """ cfg = None if not file: file = CONFIG_FILE if not CONFIG_FILE.exists(): CONFIG_DIR.mkdir(exist_ok=True) configmgr = pkg_resources.path("teachingdates.config", CONFIG_FILENAME) with configmgr as f: copyfile(f, CONFIG_FILE) print("{prog}: created new configuration file at " "'{f}'".format(prog=PROG, f=str(CONFIG_FILE))) try: with open(file) as f: cfg = yaml.safe_load(f.read()) except FileNotFoundError as e: print("{prog}: error: no such file '{file}'".format(prog=PROG, file=file)) if not cfg: raise ConfigurationError(str(file)) self.config_file = file self.config = cfg def get_period_config(self, period=None): """Get the configuration details for a teaching period. No need to specify the year as all configurations are for a single year anyway. """ period = self.period if period is None else period if period in self.config.keys(): return self.config[period] else: return None def get_paper_config(self, period=None, paper=None): """Get the configuration details for a paper. Can optionally specify the teaching period. No need to specify the year as all configurations are for a single year anyway. """ paper = self.paper if paper is None else paper period_config = self.get_period_config(period=period) if (period_config and "papers" in period_config.keys() and paper in period_config["papers"].keys()): return period_config["papers"][paper] else: return None
Show line notes below