#!/usr/bin/env python3 import argparse from datetime import datetime, timedelta from pathlib import Path import os import re import sys import urllib from bs4 import BeautifulSoup import icalevents from icalevents.icalevents import events import requests BAYERN = 1 ICAL_BASE_URL = 'https://www.schulferien.eu/downloads/ical4.php' TEMPLATE_DIR = Path( os.environ['HOME'] ) / 'Documents/MyDocuments/Work/Schule/Unterricht/templates' TEMPLATE_FILE = 'map.md' LPP_BASE_URL = 'https://www.lehrplanplus.bayern.de/fachlehrplan/gymnasium' WEEKLY_HOURS = { 'mathematik': { 5: 4, 6: 4, 7: 4, 8: 3, 9: 4, 10: 3, 11: 4 }, 'informatik': { 9: 2, 10: 2, 11: 2 }, 'nt_gym': { 6: 2, 7: 2 } } def default_year() -> int: """return the default year""" now = datetime.now() return now.year if now.month < 8 else now.year + 1 def get_holidays(land: int, year: int) -> list[icalevents.icalparser.Event]: """Get list of all holidays in a school year""" start_year = datetime(year=year - 1, month=1, day=1) end_year = datetime(year=year + 1, month=12, day=31) params = {'land': land, 'year': year, 'type': 1} uri_first_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) first_holidays = events(url=uri_first_year, start=start_year, end=end_year) params['year'] = year + 1 uri_second_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) second_holidays = events(url=uri_second_year, start=start_year, end=end_year) first_summer = next( filter(lambda e: "Sommerferien" in e.summary, first_holidays)) second_summer = next( filter(lambda e: "Sommerferien" in e.summary, second_holidays)) return [ h for h in first_holidays + second_holidays if h.start >= first_summer.start and h.start <= second_summer.start ] def fmt_holiday(holiday: icalevents.icalparser.Event) -> str: """Return a formatted holiday description""" h_start = f'{holiday.start.date().strftime("%d.%m")}' h_end = f'{holiday.end.date().strftime("%d.%m")}' return f'{h_start}-{h_end} {holiday.summary}' def get_public_holidays(land: int, start: datetime, end: datetime) -> list[icalevents.icalparser.Event]: """Get list of all public holidays in the school year""" params = {'land': land, 'year': start.year, 'type': 0} uri_first_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) params['year'] = end.year uri_second_year = ICAL_BASE_URL + '?' + urllib.parse.urlencode(params) all_public_holidays = events(url=uri_first_year, start=start, end=end) + events( url=uri_second_year, start=start, end=end) return [ e for e in all_public_holidays if e.start >= start and e.start <= end ] class LehrplanSection: def __init__(self, num, summary, duration, subsections=None): self.num = num self.summary = summary self.duration = duration self.subsections = subsections or [] def has_subsections(self) -> bool: return bool(self.subsections) def __str__(self): return f'{self.num}: {self.summary} ({self.duration}h) {self.subsections}' def __repr__(self): return f'LehrplanSection({self.num}, {self.summary}, {self.duration}h, {self.subsections})' Lehrplan = list[LehrplanSection] def get_lehrplan(subject: str, form: str) -> Lehrplan: """Retrieve the sections and expected duration from lehrplanplus""" lpp_url = '/'.join([LPP_BASE_URL, form, subject]) resp = requests.get(lpp_url, timeout=10) html = BeautifulSoup(resp.text) summary_re = re.compile(r'(.*) \(ca\. ([1-9][0-9]*) Std\.\)') lehrplan: Lehrplan = [] top_level_sections = html.find_all(**{'class': 'headline_lvl1'}) for section in top_level_sections: num = list(section.find(**{ 'class': 'head-absatz-num' }).children)[2].strip() summary = section.find(**{ 'class': 'head-absatz-title-short' }).string.strip() summary = summary.replace('\xa0', ' ') m = summary_re.match(summary) summary = m.group(1) duration = int(m.group(2)) lehrplan.append(LehrplanSection(num, summary, duration)) second_level_sections = html.find_all(**{'class': 'headline_lvl2'}) for section in second_level_sections: summary = section.find(**{ 'class': 'head-absatz-title-long' }).string.strip() summary = summary.replace('\xa0', ' ') m = summary_re.match(summary) summary = m.group(1) duration = int(m.group(2)) num = list(section.find(**{ 'class': 'head-absatz-num' }).children)[2].strip() lehrplan[int(num.split('.')[0]) - 1].subsections.append( LehrplanSection(num, summary, duration)) return lehrplan def gen_section_iter(lehrplan): sections = [] for s in lehrplan: if not s.has_subsections(): sections.append(s) else: sections.extend(s.subsections) next_section_in = sections[0].duration cur = 0 def _section_iter(weekly_hours, total_hours) -> list[LehrplanSection]: nonlocal cur if cur == len(sections) - 1: return [sections[-1]] cur_sections = [] nonlocal next_section_in # advance section if total_hours > next_section_in: if total_hours - weekly_hours < next_section_in: cur_sections.append(sections[cur]) cur += 1 next_section_in += sections[cur].duration cur_sections.append(sections[cur]) return cur_sections return _section_iter def gen_latex_table(weekly_hours: int, lehrplan: Lehrplan, school_holidays, public_holidays) -> list[str]: """generate a latex table matching lehrplan section on kalendarweeks""" rows: list[str] = [] lehrplan.append(LehrplanSection('', 'Puffer', 0)) start = school_holidays[0].end end = school_holidays[-1].start holiday_iter = iter(school_holidays[1:]) next_holiday = next(holiday_iter) section_iter = gen_section_iter(lehrplan) total_hours = 0 week_iter = start + timedelta(days=1) while week_iter < end: if week_iter > next_holiday.start: rows.append(( f'\\rowcolor{{blue!30}}{week_iter.isocalendar().week} & ' f' \\multicolumn{{2}}{{c}}{{{fmt_holiday(next_holiday)}}} \\\\' )) week_iter = next_holiday.end + timedelta(days=1) next_holiday = next(holiday_iter) continue lp_sections = section_iter(weekly_hours, total_hours) lp_level = ' \\& '.join([s.num for s in lp_sections]) lp_summary = ' \\& '.join([s.summary for s in lp_sections]) rows.append( f'{week_iter.isocalendar().week} & {lp_level} & {lp_summary} \\\\') week_iter += timedelta(days=7) total_hours += weekly_hours return rows def print_map(rows: list[str], template: Path, outfile): with open(template, 'r', encoding='utf-8') as template_file: template = template_file.readlines() template_prefix = template[:-6] template_suffix = template[-2:] print(''.join(template_prefix), file=outfile) print('\n'.join(rows), file=outfile) print(''.join(template_suffix), file=outfile) def main(): """Create a rough map template for a certain school year""" parser = argparse.ArgumentParser() parser.add_argument('subject', type=str) parser.add_argument('form', type=int) parser.add_argument('--template-dir') parser.add_argument('--year', default=default_year()) parser.add_argument('--land', default=BAYERN) parser.add_argument('-o', '--output', default=sys.stdout) args = parser.parse_args() school_holidays = get_holidays(args.land, args.year) public_holidays = get_public_holidays(args.land, school_holidays[0].end, school_holidays[-1].start) weekly_hours = WEEKLY_HOURS[args.subject][args.form] lehrplan = get_lehrplan(args.subject, str(args.form)) rows = gen_latex_table(weekly_hours, lehrplan, school_holidays, public_holidays) if args.template_dir: template_path = Path(args.template_dir) / TEMPLATE_FILE else: template_path = TEMPLATE_DIR / TEMPLATE_FILE if args.output != sys.stdout: with open(args.output, 'w', encoding='utf-8') as outfile: print_map(rows, template_path, outfile) else: print_map(rows, template_path, args.output) if __name__ == '__main__': main()