""" This file is part of python-webuntis :copyright: (c) 2013 by Markus Unterwaditzer. :license: BSD, see LICENSE for more details. """ import datetime from webuntis.utils import datetime_utils, lazyproperty, \ timetable_utils class Result(object): """Base class used to represent most API objects. :param data: Usually JSON data that should be represented. In the case of :py:class:`ListResult`, however, it might also be a list of JSON mixed with :py:class:`ListItem` objects. :param parent: (optional) A result object this result should be the child of. If given, the session will be inherited. :param session: Mandatory if ``parent`` is not supplied. Overrides the parent's inherited session. """ def __init__(self, data, parent=None, session=None): if bool(parent is None) == bool(session is None): raise TypeError('Either parent or session has to be provided.') if parent is not None and not hasattr(parent, '_session'): raise TypeError('Parent must have a _session attribute.') self._session = session or parent._session self._parent = parent self._data = data @lazyproperty def id(self): """The ID of this element. An ID is needed for the object to be hashable. Therefore a result may bring its own implementation of this method even though the original API response didn't contain any ID.""" return self._data[u'id'] if 'id' in self._data else None def __int__(self): """This is useful if the users pass a ListItem when a numerical ID is expected, so we just can put the thing through int(), regardless of what type it is.""" assert self.id is not None return self.id def __hash__(self): assert self.id is not None return hash(self.__class__.__name__) * 101 + self.id def __eq__(self, other): return type(self) is type(other) and hash(self) == hash(other) def __getstate__(self): return self._data def __setstate__(self, data): self._data = data def __str__(self): """a simple to string function: just the name or the full info -- debug only""" try: return self._data[u'name'] except KeyError: try: return self.name except AttributeError: return str(self._data) except TypeError: return str(self._data) def __repr__(self): try: return self.__class__.__qualname__ + "(" + str(self._data) + ")" except AttributeError: return self.__class__.__name__ + "(" + str(self._data) + ")" class ListItem(Result): """ListItems represent an item in a :py:class:`Result`. They don\'t contain methods to retrieve data.""" class ListResult(Result): """A list-like version of :py:class:`Result` that takes a list and returns a list of objects, containing a list value each. """ # When the Result returns an array, this is very useful. Every item of that # array will be fed to an instance of self._itemclass, with the session and # the array item as initialization arguments. #: the class which should be used to instantiate an array item. _itemclass = ListItem def filter(self, **criterions): """ Return a list of all objects, filtered by attributes:: foo = s.klassen().filter(id=1) # is kind-of the same as foo = [kl for kl in s.klassen() if kl.id == 1] # We can also use sets to match multiple values. bar = s.klassen().filter(name={'1A', '2A', '3A', '4A'}) # is kind-of the same as bar = [kl for kl in s.klassen() if kl.id in {'1A', '2A', '3A', '4A'}] # Or you can use a list: this keeps the order: the first element # of the result corresponds to the first element in the filter # Important after using combine() bar = s.klassen().filter(name=['1A', '2A', '3A', '4A']) # --> bar[0].name == '1A' # Since ``filter`` returns a ListResult itself too, we can chain # multiple calls together: bar = s.klassen().filter(id=4, name='7A') # is the same as bar = s.klassen().filter(id=4).filter(name='7A') :py:meth:`filter` is also used when using the ``in`` operator on a :py:class:`ListResult`:: we_have_it = {'name': '6A'} in s.klassen() # same as we_have_it = bool(s.klassen().filter(name='6A')) .. note:: This is only available because it looks nicer than list comprehensions or generator expressions. Depending on your usecase alternatives to this method may be faster. """ criterions = list(criterions.items()) def meets_criterions(item): """Returns true if the item meets the criterions""" for key, value in criterions: # if the attribute value isn't one we're looking for attribute = getattr(item, key) if attribute == value: continue elif isinstance(value, set) and attribute in value: continue else: return False return True if isinstance(criterions[0][1], list): return type(self)( parent=self, data=[self.filter(**{key: v})[0] for key, values in criterions for v in values ]) return type(self)( parent=self, data=[item for item in self if meets_criterions(item)] ) def __contains__(self, criterion): if isinstance(criterion, self._itemclass): return any(item is criterion for item in self) return bool(self.filter(**criterion)) def __getitem__(self, i): """Makes the object iterable and behave like a list""" data = self._data[i] # fails if there is no such item if type(data) is not self._itemclass: data = self._data[i] = self._itemclass( parent=self, data=data ) return data def __len__(self): """Return the length of the items""" return len(self._data) def __hash__(self): raise NotImplementedError() def __eq__(self, other): return type(other) is type(self) and other._data == self._data def __str__(self): """a simple to string function: a list of results -- debug only""" return "[" + ", ".join(str(d) for d in self._data) + "]" def __repr__(self): """a simple to string function: a list of results -- debug only""" try: return self.__class__.__qualname__ + "[" + ", ".join(repr(d) for d in self._data) + "]" except AttributeError: return self.__class__.__name__ + "[" + ", ".join(repr(d) for d in self._data) + "]" class DepartmentObject(ListItem): """Represents a department""" @lazyproperty def name(self): """short name such as *R1A*""" return self._data[u'name'] @lazyproperty def long_name(self): """Long name, such as *Raum Erste A*. Not predictable.""" return self._data[u'longName'] class DepartmentList(ListResult): """A list of departments, in form of :py:class:`DepartmentObject` instances.""" _itemclass = DepartmentObject class HolidayObject(ListItem): """Represents a single holiday.""" @lazyproperty def start(self): """The start date of the holiday, as a datetime object.""" return datetime_utils.parse_date(self._data[u'startDate']) @lazyproperty def end(self): """The end of the holiday""" return datetime_utils.parse_date(self._data[u'endDate']) @lazyproperty def name(self): """Name, such as *Nationalfeiertag*.""" return self._data[u'longName'] @lazyproperty def short_name(self): """Abbreviated form of the name""" return self._data[u'name'] class HolidayList(ListResult): """A list of holidays, in form of :py:class:`HolidayObject` instances.""" _itemclass = HolidayObject class ColorMixin: """Interface support fore/back color""" @lazyproperty def forecolor(self): """The foreground color used in the web interface and elsewhere""" return self._data[self.name][u'foreColor'] @lazyproperty def backcolor(self): """The background color used in the web interface and elsewhere""" return self._data[self.name][u'backColor'] class KlassenObject(ListItem, ColorMixin): """Represents a school class.""" @lazyproperty def name(self): """Name of class""" return self._data[u'name'] @lazyproperty def long_name(self): """Long name of class""" return self._data[u'longName'] class KlassenList(ListResult): """A list of school classes, in form of :py:class:`KlassenObject` instances.""" _itemclass = KlassenObject class PeriodObject(ListItem): """Represents a time range, where lessons/subjects may be held.""" @lazyproperty def start(self): """The start date/time of the period, as datetime object.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'startTime'] ) @lazyproperty def end(self): """The end date/time of the period.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'endTime'] ) @lazyproperty def klassen(self): """A :py:class:`KlassenList` containing the classes which are attending this period.""" return self._session.klassen(from_cache=True).filter( id=[kl[u'id'] for kl in self._data[u'kl']] ) @lazyproperty def teachers(self): """A list of :py:class:`TeacherObject` instances, which are attending this period.""" return self._session.teachers(from_cache=True).filter( id=[te[u'id'] for te in self._data[u'te']] ) @lazyproperty def subjects(self): """A :py:class:`SubjectList` containing the subjects which are topic of this period. This is not used for things like multiple language lessons (*e.g.* Latin, Spanish, French) -- each of those will get placed in their own period.""" return self._session.subjects(from_cache=True).filter( id=[su[u'id'] for su in self._data[u'su']] ) @lazyproperty def rooms(self): """The rooms (:py:class:`RoomList`) where this period is taking place at. This also is not used for multiple lessons, but rather for a single lesson that is actually occuring at multiple locations (?).""" return self._session.rooms(from_cache=True).filter( id=[ro[u'id'] for ro in self._data[u'ro']] ) @lazyproperty def code(self): """May be: - ``None`` -- There's nothing special about this period. - ``"cancelled"`` -- Cancelled - ``"irregular"`` -- Substitution/"Supplierung"/Not planned event """ code = self._data.get(u'code', None) if code in (None, u'cancelled', u'irregular'): return code return None @lazyproperty def original_teachers(self): """ Support for original teachers """ try: return self._session.teachers(from_cache=True).filter(id=[te[u'orgid'] for te in self._data[u'te']]) except KeyError: pass return [] @lazyproperty def original_rooms(self): """ Support for original rooms """ try: return self._session.rooms(from_cache=True).filter(id=[ro[u'orgid'] for ro in self._data[u'ro']]) except KeyError: pass return [] @lazyproperty def type(self): """May be: - ``"ls"`` -- Normal lesson - ``"oh"`` -- Office hour - ``"sb"`` -- Standby - ``"bs"`` -- Break Supervision - ``"ex"`` -- Examination """ return self._data.get(u'lstype', u'ls') class PeriodList(ListResult): """Aka timetable, a list of periods, in form of :py:class:`PeriodObject` instances.""" _itemclass = PeriodObject def to_table(self, dates=None, times=None): """ Creates a table-like structure out of the periods. Useful for rendering timetables in HTML and other markup languages. Check out the example from the repository for clarification. :param dates: An iterable of :py:class:`datetime.date` objects that definetly should be included in the table. If this parameter is ``None``, the timetable is just as wide as it has to be, leaving out days without periods. :param times: An iterable of :py:class:`datetime.time` objects that definetly should be included in the table. If this parameter is ``None``, the timetable is just as tall as it has to be, leaving out hours without periods. :returns: A list containing "rows", which in turn contain "hours", which contain :py:class:`webuntis.objects.PeriodObject` instances which are happening at the same time. """ return timetable_utils.table(self, dates=dates, times=times) def combine(self, combine_breaks=True): """ Combine consecutive entries :param combine_breaks: combine of breaks :return: """ return timetable_utils.combine(self, {'date', 'activityType', 'su', 'kl'}, combine_breaks) class RoomObject(ListItem, ColorMixin): """Represents a physical room. Such as a classroom, but also the physics lab or whatever. """ @lazyproperty def name(self): """The short name of the room. Such as PHY.""" return self._data[u'name'] @lazyproperty def long_name(self): """The long name of the room. Such as "Physics lab".""" return self._data[u'longName'] class RoomList(ListResult): """A list of rooms, in form of :py:class:`RoomObject` instances.""" _itemclass = RoomObject class SchoolyearObject(ListItem): """Represents a schoolyear.""" @lazyproperty def name(self): """"2010/2011\"""" return self._data[u'name'] @lazyproperty def start(self): """The start date of the schoolyear, as datetime object""" return datetime_utils.parse_date(self._data[u'startDate']) @lazyproperty def end(self): """The end date""" return datetime_utils.parse_date(self._data[u'endDate']) @lazyproperty def is_current(self): """ Boolean, check if this is the current schoolyear:: >>> import webuntis >>> s = webuntis.Session(...).login() >>> y = s.schoolyears() >>> y.current.id 7 >>> y.current.is_current True >>> y.filter(id=y.current.id).is_current True """ return self == self._parent.current class SchoolyearList(ListResult): """A list of schoolyears, in form of :py:class:`SchoolyearObject` instances.""" _itemclass = SchoolyearObject @lazyproperty def current(self): """Returns the current schoolyear in form of a :py:class:`SchoolyearObject`""" current_data = self._session._request(u'getCurrentSchoolyear') current = self.filter(id=current_data[u'id'])[0] return current class SubjectObject(ListItem, ColorMixin): """Represents a subject.""" @lazyproperty def name(self): """Short name of subject, such as *PHY*""" return self._data[u'name'] @lazyproperty def long_name(self): """Long name of subject, such as *Physics*""" return self._data[u'longName'] class SubjectList(ListResult): """A list of subjects, in form of :py:class:`SubjectObject` instances.""" _itemclass = SubjectObject class PersonObject(ListItem): """Represents a person (teacher or student).""" @lazyproperty def fore_name(self): """fore name of the person""" return self._data[u'foreName'] @lazyproperty def long_name(self): """surname of person""" return self._data[u'longName'] surname = long_name @lazyproperty def name(self): """full name of the person""" return self._data[u'name'] class TeacherObject(PersonObject): """Represents a teacher.""" @lazyproperty def title(self): """title of the teacher""" return self._data[u'title'] @lazyproperty def full_name(self): """full name of teacher (title, forname, longname""" return " ".join((self.title, self.fore_name, self.long_name)).strip() class TeacherList(ListResult): """A list of teachers, in form of :py:class:`TeacherObject` instances.""" _itemclass = TeacherObject class ColorInfo(Result, ColorMixin): """ An object containing information about a lesson type or a period code:: >>> import webuntis >>> s = webuntis.Session(...).login() >>> lstype = s.statusdata().lesson_types[0] >>> lstype.name 'ls' >>> lstype.forecolor '000000' >>> lstype.backcolor 'ee7f00' :: >>> pcode = s.statusdata().period_codes[0] >>> pcode.name 'cancelled' >>> pcode.forecolor 'FFFFFF' >>> pcode.backcolor 'FF0000' """ @lazyproperty def id(self): return hash(self.__class__.__name__ + self.name) @lazyproperty def name(self): """The name of the LessonType or PeriodCode""" return list(self._data.items())[0][0] class StatusData(Result): """Information about lesson types and period codes and their colors.""" @lazyproperty def lesson_types(self): """A list of :py:class:`ColorInfo` objects, containing information about all lesson types defined :rtype: `list` [ColorInfo] """ return [ ColorInfo(parent=self, data=data) for data in self._data[u'lstypes'] ] @lazyproperty def period_codes(self): """A list of :py:class:`ColorInfo` objects, containing information about all period codes defined :rtype: `list` [ColorInfo] """ return [ ColorInfo(parent=self, data=data) for data in self._data[u'codes'] ] class TimeStampObject(Result): """Information about last change of data -- timestamp (given in milliseconds)""" @lazyproperty def date(self): """ get timestamp as python datetime object :return: datetime.datetime """ return datetime.datetime.fromtimestamp(self._data / 1000) class SubstitutionObject(PeriodObject): """Information about substitution.""" @lazyproperty def type(self): """type of substitution cancel cancellation subst teacher substitution add additional period shift shifted period rmchg room change :rtype: str """ return self._data[u'type'] @lazyproperty def reschedule_start(self): """The start of the rescheduled substitution (or None) :return: datetime.datetime """ try: return datetime_utils.parse_datetime(self._data[u'reschedule'][u'date'], self._data[u'reschedule'][u'startTime']) except KeyError: return None @lazyproperty def reschedule_end(self): """The end of the rescheduled substitution (or None) :return: datetime.datetime """ try: return datetime_utils.parse_datetime(self._data[u'reschedule'][u'date'], self._data[u'reschedule'][u'endTime']) except KeyError: return None class SubstitutionList(ListResult): """A list of substitutions in form of :py:class:`SubstitutionObject` instances.""" _itemclass = SubstitutionObject def combine(self, combine_breaks=True): """ Combine consecutive entries :param combine_breaks: combine of breaks :return: """ return timetable_utils.combine(self, {'date', 'type', 'kl', 'su'}, combine_breaks, lambda p: (p['type'], str(p[u'te'] and p[u'te'][0][u'name']), p[u'date'], p[u'startTime'])) class TimeUnitObject(Result): """Information about the time grid""" @lazyproperty def name(self): """Name of Timeunit""" return self._data[u'name'] @lazyproperty def start(self): return datetime_utils.parse_time( self._data[u'startTime'] ).time() @lazyproperty def end(self): return datetime_utils.parse_time( self._data[u'endTime'] ).time() class TimegridDayObject(Result): """Information about one day in the time grid""" @lazyproperty def day(self): return self._data[u'day'] @lazyproperty def dayname(self): names = {1: "sunday", 2: "monday", 3: "tuesday", 4: "wednesday", 5: "thursday", 6: "friday", 7: "saturday"} return names[self._data[u'day']] @lazyproperty def time_units(self): return [ TimeUnitObject(parent=self, data=data) for data in self._data[u'timeUnits'] ] class TimegridObject(ListResult): """A list of TimegridDayObjects """ _itemclass = TimegridDayObject class StudentObject(PersonObject): """Represents a student.""" @lazyproperty def full_name(self): """full name of student (forname, longname)""" return " ".join((self.fore_name, self.long_name)).strip() @lazyproperty def gender(self): return self._data[u'gender'] @lazyproperty def key(self): return self._data[u'key'] class StudentsList(ListResult): """A list of students""" _itemclass = StudentObject class ExamTypeObject(Result): """Represents an Exam Type.""" @lazyproperty def long_name(self): """Long name""" return self._data[u'longName'] @lazyproperty def name(self): """name""" return self._data[u'name'] @lazyproperty def show_in_timetable(self): """show this exam type in the timetable""" return self._data[u'showInTimetable'] class ExamTypeList(ListResult): """A list of exam types""" _itemclass = ExamTypeObject class ExamObject(Result): """Represents an Exam.""" # classes list of classes # teachers list of teachers # students list of students # subject @lazyproperty def start(self): """The start date/time of the period, as datetime object.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'startTime'] ) @lazyproperty def end(self): """The end date/time of the period.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'endTime'] ) @lazyproperty def klassen(self): """A :py:class:`KlassenList` containing the classes which are attending this period.""" return self._session.klassen(from_cache=True).filter( id=set(self._data[u'classes']) ) @lazyproperty def teachers(self): """A list of :py:class:`TeacherObject` instances, which are attending this period.""" return self._session.teachers(from_cache=True).filter( id=set(self._data[u'teachers']) ) @lazyproperty def subject(self): """A :py:class:`SubjectObject` with the subject which are topic of this period.""" return self._session.subjects(from_cache=True).filter(id=self._data[u'subject'])[0] @lazyproperty def students(self): """A list of :py:class:`StudentObject` instances, which are attending this period.""" return self._session.students(from_cache=True).filter( id=set(self._data[u'students']) ) class ExamsList(ListResult): """A list of exams.""" _itemclass = ExamObject class AbsenceObject(Result): """Represents an absence. Attention: if there are multiple teachers/groups at the same time -> multiple entries for the same student, but the absentTime is only set for one (the first?) entry. """ @lazyproperty def student(self): """ doku says: student ID, but it is the students KEY :return: """ return self._session.students(from_cache=True).filter(key=self._data[u'studentId'])[0] @lazyproperty def subject(self): """@TODO: untested - always empty""" try: sid = int(self._data[u'subjectId']) except ValueError: return "" return self._session.subjects(from_cache=True).filter(id=sid)[0] @lazyproperty def teachers(self): """@TODO: untested - always empty""" try: tes = list(int(te) for te in self._data[u'teacherIds'] if te) except ValueError: return [] return self._session.teachers(from_cache=True).filter(id=tes) @lazyproperty def student_group(self): try: return self._data[u'studentGroup'] except KeyError: return '' @lazyproperty def checked(self): return self._data[u'checked'] @lazyproperty def name(self): """Name of absent student""" return self.student.full_name @lazyproperty def start(self): """The start date/time of the period, as datetime object.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'startTime'] ) @lazyproperty def end(self): """The end date/time of the period.""" return datetime_utils.parse_datetime( self._data[u'date'], self._data[u'endTime'] ) @lazyproperty def reason(self): try: return self._data[u'absenceReason'] except KeyError: return '' @lazyproperty def time(self): try: return int(self._data[u'absentTime']) except KeyError: return 0 @lazyproperty def status(self): try: return self._data[u'excuseStatus'] except KeyError: return '' class AbsencesList(ListResult): """A list of absences.""" _itemclass = AbsenceObject def __init__(self, data, parent=None, session=None): # the data is a dict() with just one key data = data[u'periodsWithAbsences'] Result.__init__(self, data, parent, session) class ClassRegEvent(Result): """Represents an ClassRegEvent.""" @lazyproperty def student(self): """ doku says: student ID, but it is the students KEY :return: """ return self._session.students(from_cache=True).filter(key=self._data[u'studentid'])[0] @lazyproperty def sur_name(self): """sur name of the person""" return self._data[u'surname'] @lazyproperty def fore_name(self): """fore name of the person""" return self._data[u'forname'] @lazyproperty def name(self): """fore name of the person""" return " ".join((self.sur_name, self.fore_name)) @lazyproperty def reason(self): """reason of the classregevent""" return self._data[u'reason'] @lazyproperty def text(self): """text of the classregevent""" return self._data[u'text'] @lazyproperty def date(self): """the date of the classregevent.""" return datetime_utils.parse_date(self._data[u'date']) @lazyproperty def subject(self): """the subject of the classregevent.""" return self._data[u'subject'] @lazyproperty def category(self): """which category""" return self._session.class_reg_categories(from_cache=True).filter( id=set(self._data[u'categoryId']) )[0] class ClassRegEventList(ListResult): """A list of ClassRegEvents.""" _itemclass = ClassRegEvent class ClassRegCategory(Result): """Represents an ClassRegCategory.""" @lazyproperty def name(self): """name of category""" return self._data[u'name'] @lazyproperty def longname(self): """longname of category""" return self._data[u'longName'] @lazyproperty def group(self): """group""" return self._session.class_reg_category_groups().filter(id=self._data[u'groupId'])[0] class ClassRegCategoryList(ListResult): """A list of ClassRegCategories.""" _itemclass = ClassRegCategory class ClassRegCategoryGroup(Result): """Represents an ClassRegCategoryGroup.""" @lazyproperty def name(self): """name of group""" return self._data[u'name'] class ClassRegCategoryGroupList(ListResult): """A list of ClassRegCategoriesGroups.""" _itemclass = ClassRegCategoryGroup