aboutsummaryrefslogtreecommitdiff
path: root/lessons.py
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-04-05 12:46:23 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2023-04-05 12:46:23 +0200
commitce4d9f762b57185446506112868e70bfe8f9b8ad (patch)
tree25469b5ad3f15ae5bbac0e39f9dd5cf33df14fc8 /lessons.py
downloadavail-lessons-ce4d9f762b57185446506112868e70bfe8f9b8ad.tar.gz
avail-lessons-ce4d9f762b57185446506112868e70bfe8f9b8ad.zip
initial commit
Diffstat (limited to 'lessons.py')
-rwxr-xr-xlessons.py177
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()