import logging import requests from collections import namedtuple from datetime import datetime, timedelta eduTypes = {1: {'en': 'Teaching session', 'sv': 'Lektion'}, 2: {'en': 'Seminar', 'sv': 'Seminarium'}, 3: {'en': 'Lecture', 'sv': 'Föreläsning'}, 4: {'en': 'Laboratory session', 'sv': 'Laboration'}, 5: {'en': 'Presentation', 'sv': 'Redovisning'}, 6: {'en': 'Introduction', 'sv': 'Introduktion'}, 7: {'en': 'Exercise', 'sv': 'Övning'}, 8: {'en': 'Supervision', 'sv': 'Handledning'}, 9: {'en': 'Tutorial', 'sv': 'Räknestuga'}, 10: {'en': 'Project', 'sv': 'Projekt'}, 11: {'en': 'Workshop', 'sv': 'Workshop'}, 12: {'en': 'Q & A', 'sv': 'Frågestund'}, 13: {'en': 'Deadline', 'sv': 'Deadline'}, None: None, } def parseSemester(semester): halves = {'1': 'vt', '2': 'ht'} semester = str(semester) year = semester[:4] half = semester[4] return f"{halves[half]}{year}" class Daisy: def __init__(self, config): self.auth = (config['user'], config['password']) self.headers = {'Accept': 'application/json'} self.url = config['url'].rstrip('/') self.logger = logging.getLogger('play-daemon') def _get(self, path, params={}): """ Make a request to the daisy API and return the result as a dict. Throws an exception if the HTTP response indicates an error. """ r = requests.get(f'{self.url}{path}', params, headers=self.headers, auth=self.auth) r.raise_for_status() return r.json() def get_booking(self, start_time, end_time, room): """ Return the booking with the best overlap with the given times for the given room. Returned bookings are fully resolved with respect to users, courses, etc. """ fmt = '%Y-%m-%d' start_day = start_time.strftime(fmt) end_day = (end_time + timedelta(days=1)).strftime(fmt) params = {'start': start_day, 'end': end_day, 'room': room} bookings = self._get('/schedule', params=params) # Resolve all the numeric IDs in a booking: for booking in bookings: courses = [] for course in booking['courseSegmentInstances']: courses.append(self.get_course(course['id'])) booking['courseSegmentInstances'] = sorted(courses) teachers = [] for teacher in booking['teachers']: teachers.append(self.get_person(teacher['id'])) booking['teachers'] = sorted(teachers) if booking['bookedBy']: booking['bookedBy'] = self.get_person(booking['bookedBy']) # This should be replaced by an API lookup booking['educationalType'] = eduTypes[booking['educationalType']] # If a lecture belongs to more than one course, there will be one # booking for each course, all with identical times. # Sorting on courseSegmentInstance ensures that we return a consistent # course in such cases. def booking_sort(booking): if booking['courseSegmentInstances']: return booking['courseSegmentInstances'][0]['designation'] return '' Fit = namedtuple('Fit', ['booking', 'overlap']) best = Fit(None, timedelta()) self.logger.info('Matching bookings for room %s at %s - %s', room, start_time, end_time) for booking in sorted(bookings, key=booking_sort): b_start = datetime.fromisoformat(booking['start']) b_end = datetime.fromisoformat(booking['end']) self.logger.debug('Booking %s for course %s: %s - %s', booking['id'], booking['courseSegmentInstances'], b_start, b_end) range_start = max(start_time, b_start) range_end = min(end_time, b_end) overlap = range_end - range_start self.logger.debug('Time ranges overlap by %s', overlap) self.logger.debug('Previous best overlap: %s', best.overlap) if overlap > best.overlap: self.logger.debug('Saving %s as current best fit', booking['id']) best = Fit(booking, overlap) bestfit = best.booking if bestfit: self.logger.info( 'Best booking fit was %s for course %s, overlap %s', bestfit['id'], bestfit['courseSegmentInstances'], best.overlap) else: self.logger.info('No suitable booking found.') return best.booking def get_person(self, person_id): """ Return the SU.SE username associated with a user. If none exists, return the user's first and last names instead. Throws an exception if a user has more than one SU.SE username. """ usernames = [name['username'] for name in self._get(f'/person/{person_id}/usernames') if name['realm'] == 'SU.SE'] if len(usernames) == 0: person = self._get(f'/person/{person_id}') return f'{person["firstName"]} {person["lastName"]}' if len(usernames) > 1: raise Exception( f'More than one SU.SE username for {person_id}: {usernames}') return usernames[0] def get_course(self, course_id): """ Return the course designation for the given course ID. """ course = self._get(f'/courseSegment/{course_id}') return {'designation': course['designation'], 'semester': parseSemester(course['semester'])} def get_room_name(self, room_id): """ Return the name of the room with the given ID. """ location = self._get(f'/location/{room_id}') return location['designation']