diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-04-05 12:46:23 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-04-05 12:46:23 +0200 |
| commit | ce4d9f762b57185446506112868e70bfe8f9b8ad (patch) | |
| tree | 25469b5ad3f15ae5bbac0e39f9dd5cf33df14fc8 /lessons.py | |
| download | avail-lessons-ce4d9f762b57185446506112868e70bfe8f9b8ad.tar.gz avail-lessons-ce4d9f762b57185446506112868e70bfe8f9b8ad.zip | |
initial commit
Diffstat (limited to 'lessons.py')
| -rwxr-xr-x | lessons.py | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/lessons.py b/lessons.py new file mode 100755 index 0000000..ea17e06 --- /dev/null +++ b/lessons.py @@ -0,0 +1,177 @@ +#!/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 usefull shortcuts for the output format + day = period.start.date() + week = day.isocalendar().week # pylint: disable=unused-variable + + output = config.get('fmt') or '{week} {day.strftime(\'%d.%m\')}' + # 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 wirtten + # config), therefore potential code execution is not a real concern. + output = eval(F'f"{output}"') # pylint: disable=eval-used + + 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: + output += f' {event.summary} &' + # strip the trailing ' &' suffix + output = output[:-2] + 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) + + 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 __name__ == '__main__': + main() |
