#!/usr/bin/env python3 """Print your future lessons in a specific class""" import argparse from dataclasses import dataclass import datetime from getpass import getpass import pathlib import os import sys import typing as t import icalevents from icalevents.icalevents import events import toml import webuntis @dataclass class SessionCredentials: """Data required to create a new webuntis session""" school: str server: str username: str password: str def __init__(self, config: dict): self.school = config.get('school') or input('Webuntis School: ') self.server = config.get('server') or input('Webuntis Server: ') self.username = config.get('user') or input('Webuntis User: ') self.password = config.get('password') or getpass( prompt='Webuntis Password: ') def find_config() -> t.Optional[pathlib.Path]: """Search for a config file""" path = pathlib.Path('config') if path.is_file(): return path config_dir = pathlib.Path( os.environ.get('XDG_CONFIG_HOME', os.environ['HOME'] + '/.config')) path = config_dir / 'avail-lessons' / 'config' if path.is_file(): return path return None def load_config(path="") -> dict: """Load a toml config file""" path = path or find_config() if not path: return {} with open(path, 'r', encoding='utf-8') as config_file: config = toml.load(config_file) return config def load_events(config: dict) -> list[icalevents.icalparser.Event]: """Retrieve all events from the specified ics calendars""" evs = [] for uri in config['icalendars']: if uri.startswith("http"): evs += events(url=uri, start=config['start'], end=config['end']) elif pathlib.Path(uri).is_file(): evs += events(file=uri, start=config['start'], end=config['end']) return evs def format_period(period: webuntis.objects.PeriodObject, evs: list[icalevents.icalparser.Event], config: dict) -> str: """Format a given period""" # Provide some useful shortcuts for the output format date = period.start.date() week = date.isocalendar().week # pylint: disable=unused-variable period_events = "" period_evs = [ ev for ev in evs if ev.start.timestamp() <= period.start.timestamp() <= ev.end.timestamp() ] if period_evs: for event in period_evs: period_events += f' {event.summary} &' # strip the trailing ' &' suffix period_events = period_events[:-2] output = config.get( 'fmt') or '{week} {date.strftime(\'%d.%m\')} {period_events}' # Using eval here instead of string.format() is potentially dangerous. # However, I do not consider this an actual thread because this is an # obscure script run with only trusted inputs (cli arguments, manual written # config), therefore potential code execution is not a real concern. output = eval(F'f"{output}"') # pylint: disable=eval-used return output def filter_periods(timetable: webuntis.objects.PeriodList, config: dict) -> list[webuntis.objects.PeriodObject]: """Filter the timetable for our periods""" return [ p for p in timetable if p.subjects.filter( # type: ignore # pylint: disable=no-member name=config['subject']) and p.teachers.filter( # pylint: disable=no-member name=config['teacher']) ] def main(): """Programm entry point loading data from webuntis and printing the remaining lessons""" parser = argparse.ArgumentParser() parser.add_argument('--school', help='The shool identifier used by webuntis') parser.add_argument('--server', help='The webuntis server') parser.add_argument('--user', help='The webuntis login username') parser.add_argument('--password', help='The webuntis login password') parser.add_argument('--config', help='Path to a configuration file') parser.add_argument('--teacher', help='The teacher abbreviation used in webuntis') parser.add_argument('--subject', help='The subject abbreviation used in webuntis') parser.add_argument('--class', help='The class name') parser.add_argument('--show-events', action='store_true', help='Show ICS calendar events during the lessons') args = parser.parse_args() config = {'start': datetime.datetime.now()} config.update(load_config(args.config)) # command line arguments override the values in the config file config.update({k: v for k, v in args.__dict__.items() if v}) if 'class' not in config or 'subject' not in config: print('Class and subject must be provided') sys.exit(1) # use the webuntis username as teacher fallback if 'teacher' not in config: config['teacher'] = config['username'] session_info = SessionCredentials(config) with webuntis.Session(useragent="muhq's avail-lessons script", **session_info.__dict__).login() as session: # Unfortunately pylint does not understand the type (_itemclass) # of the webuntis ListResults, therfore we supress the errors when # accessing members of the _itemclass. year = [ y for y in session.schoolyears() if y.start <= config['start'] <= y.end # pylint: disable=no-member ][0] if 'end' not in config: config['end'] = year.end klasse = session.klassen().filter(name=config['class'])[0] # pylint: disable=no-member timetable = session.timetable_extended(klasse=klasse, start=config['start'], end=config['end']) evs = [] if args.show_events: evs = load_events(config) if 'output_header' in config: print(config['output_header']) for period in filter_periods(timetable, config): if period.code: print(f'WARNING: period {period.start} is {period.code}', file=sys.stderr) print(format_period(period, evs, config)) if 'output_footer' in config: print(config['output_footer']) if __name__ == '__main__': main()