Module librusapi.timetable

Expand source code
from librusapi.token import Token
from librusapi.urls import TIMETABLE_URL, HEADERS
from librusapi.exceptions import AuthorizationError
from requests import Session
from bs4 import BeautifulSoup as bs  # type: ignore
from bs4.element import Tag  # type: ignore
from datetime import datetime, timedelta
from dataclasses import dataclass
from functools import total_ordering, cached_property
from typing import Tuple, Optional, Iterator, Union
from librusapi.helpers import sanitize


class Week:
    """Container for a week.

    Use it with str() to get the YYYY-MM-DD_YYYY-MM-DD representation
    used in librus timetable requests.

    Attributes:
        start: Any day in a week. Will be automatically changed to a Monday.
        end: End of the week.
    """

    def __init__(self, start: datetime):
        if start.weekday() != 0:
            start = start - timedelta(days=start.weekday())
        self.start = start
        """Start of the week."""

    @cached_property
    def end(self) -> datetime:
        """End of the week."""
        return self.start + timedelta(days=6)

    @cached_property
    def ___str__(self):
        fmt = "%Y-%m-%d"
        return self.start.strftime(fmt) + "_" + self.end.strftime(fmt)

    def __str__(self):
        return self.___str__


@total_ordering
@dataclass
class LessonUnit:
    """Stores a single unit on a timetable.

    You can use comparisons to determine which lesson starts first.

    Attributes:
        name: Lesson's name.
        teacher: Teacher's full name.
        classroom: Class' room string.
        info: Additional info attached to a unit.
            Can be for example: 'dzien wolny od szkoly', 'zastepstwo'
        start: Full `datetime` of lesson's start time
        duration: Duration in `timedelta`
        week: `Week` of the lesson

    """

    name: str
    """Lesson's name"""
    teacher: str
    """Teacher's full name."""
    classroom: Optional[str]
    """ Class' room."""
    info: Optional[str]
    """Additional info attached to a unit. """
    start: datetime
    """Full `datetime` of lesson's start time"""
    duration: timedelta
    """Lesson's duration"""

    def __lt__(self, other):
        """Compares dates of units"""
        if not isinstance(other, LessonUnit):
            return NotImplemented
        return self.start < other.start

    def __eq__(self, other):
        """Compares dates of units"""
        if not isinstance(other, LessonUnit):
            return NotImplemented
        return self.start == other.start

    @cached_property
    def week(self):
        """`Week` of the lesson"""
        return Week(self.start)


def _get_html(token: Token, week: Week) -> str:
    s = Session()
    data = {"tydzien": str(week)}
    resp = token.post(TIMETABLE_URL, data)
    return resp.text


def _process_entry_text(et: Tag) -> Tuple[str, Optional[str], str]:
    """Process the 'text' part of a lesson unit html"""
    text_data = et.find_all(text=True)
    # remove unnecessary newlines found
    if len(text_data) == 4:
        del text_data[0]
        del text_data[1]
    class_name, teacher_and_classroom = text_data
    teacher_and_classroom = sanitize(teacher_and_classroom)
    if " s. " in teacher_and_classroom:
        teacher, classroom = teacher_and_classroom.split(" s. ")
        teacher = teacher[1:]
    else:
        teacher = teacher_and_classroom
        classroom = None
    teacher = sanitize(teacher)
    class_name = sanitize(class_name)
    return class_name, classroom, teacher


def _process_entry_time(e: Tag) -> Tuple[datetime, timedelta, Optional[str]]:
    def _parse_time(time: str):
        """Parse a HH:MM string to datetime"""
        return datetime.strptime(time, "%H:%M")

    day = e.attrs.get("data-date")
    time_from = _parse_time(e.attrs.get("data-time_from"))
    time_to = _parse_time(e.attrs.get("data-time_to"))
    info = e.find("div", attrs={"class": "plan-lekcji-info"})
    if info:
        info = info.text
    start = datetime.strptime(day, "%Y-%m-%d") + timedelta(
        hours=time_from.hour, minutes=time_from.minute
    )
    duration = time_to - time_from
    return start, duration, info


def _process_entry(e: Tag, et: Tag) -> LessonUnit:
    start, duration, info = _process_entry_time(e)
    class_name, classroom, teacher = _process_entry_text(et)
    return LessonUnit(
        name=class_name,
        teacher=teacher,
        classroom=classroom,
        start=start,
        duration=duration,
        info=info,
    )


def _parse_html(html: str) -> Iterator[LessonUnit]:
    doc = bs(html, "lxml")
    no_access = doc.find("h2")
    if no_access and no_access.text == "Brak dostępu":
        raise AuthorizationError("Token invalid or expired.")
    entries = doc.find_all("td", {"id": "timetableEntryBox"})
    for e in entries:
        et = e.find("div", attrs={"class": "text"})
        if not et:
            continue
        yield _process_entry(e, et)


def lesson_units(
    token: Token, week: Optional[Union[datetime, Week]] = datetime.now()
) -> Iterator[LessonUnit]:
    """Generator for lesson units.

    Retrieves HTML and then parses it using Beautifulsoup.

    Args:
        token: Librus token.
            Can be retrieved from `librusapi.token.get_token`.
        week: Determines which week will be fetched from librus
            Either a `Week` or any day which will be converted to `Week`.
            Defaults to `datetime.now()`
    Returns:
        Generator of `LessonUnit` that parses one instance at a time.
        Results are unordered.
    Raises:
        requests.exceptions.HTTPError: When an error that is
            unrelated to authenticaiton occurs.
        AuthorizationError: When authorization using the token fails.
    Example:
    >>> lesson_units = lesson_units(token)
    >>> for lu in lesson_units:
    ...     print(lu.name, lu.teacher, lu.classroom, lu.info, lu.start, lu.duration, sep='\\n')
    ...     break
    Math
    John Smith
    111
    None
    2021-01-11 08:00:00
    0:45:00
    """
    if not week:
        week = datetime.now()

    if isinstance(week, datetime):
        week = Week(week)
    html = _get_html(token, week)
    return _parse_html(html)

Functions

def lesson_units(token: Token, week: Union[datetime.datetime, Week, NoneType] = datetime.datetime(2021, 2, 8, 21, 31, 59, 172612)) ‑> Iterator[LessonUnit]

Generator for lesson units.

Retrieves HTML and then parses it using Beautifulsoup.

Args

token
Librus token. Can be retrieved from get_token().
week
Determines which week will be fetched from librus Either a Week or any day which will be converted to Week. Defaults to datetime.now()

Returns

Generator of LessonUnit that parses one instance at a time. Results are unordered.

Raises

requests.exceptions.HTTPError
When an error that is unrelated to authenticaiton occurs.
AuthorizationError
When authorization using the token fails.

Example:

>>> lesson_units = lesson_units(token)
>>> for lu in lesson_units:
...     print(lu.name, lu.teacher, lu.classroom, lu.info, lu.start, lu.duration, sep='\n')
...     break
Math
John Smith
111
None
2021-01-11 08:00:00
0:45:00
Expand source code
def lesson_units(
    token: Token, week: Optional[Union[datetime, Week]] = datetime.now()
) -> Iterator[LessonUnit]:
    """Generator for lesson units.

    Retrieves HTML and then parses it using Beautifulsoup.

    Args:
        token: Librus token.
            Can be retrieved from `librusapi.token.get_token`.
        week: Determines which week will be fetched from librus
            Either a `Week` or any day which will be converted to `Week`.
            Defaults to `datetime.now()`
    Returns:
        Generator of `LessonUnit` that parses one instance at a time.
        Results are unordered.
    Raises:
        requests.exceptions.HTTPError: When an error that is
            unrelated to authenticaiton occurs.
        AuthorizationError: When authorization using the token fails.
    Example:
    >>> lesson_units = lesson_units(token)
    >>> for lu in lesson_units:
    ...     print(lu.name, lu.teacher, lu.classroom, lu.info, lu.start, lu.duration, sep='\\n')
    ...     break
    Math
    John Smith
    111
    None
    2021-01-11 08:00:00
    0:45:00
    """
    if not week:
        week = datetime.now()

    if isinstance(week, datetime):
        week = Week(week)
    html = _get_html(token, week)
    return _parse_html(html)

Classes

class LessonUnit (name: str, teacher: str, classroom: Optional[str], info: Optional[str], start: datetime.datetime, duration: datetime.timedelta)

Stores a single unit on a timetable.

You can use comparisons to determine which lesson starts first.

Attributes

name
Lesson's name.
teacher
Teacher's full name.
classroom
Class' room string.
info
Additional info attached to a unit. Can be for example: 'dzien wolny od szkoly', 'zastepstwo'
start
Full datetime of lesson's start time
duration
Duration in timedelta
week
Week of the lesson
Expand source code
@total_ordering
@dataclass
class LessonUnit:
    """Stores a single unit on a timetable.

    You can use comparisons to determine which lesson starts first.

    Attributes:
        name: Lesson's name.
        teacher: Teacher's full name.
        classroom: Class' room string.
        info: Additional info attached to a unit.
            Can be for example: 'dzien wolny od szkoly', 'zastepstwo'
        start: Full `datetime` of lesson's start time
        duration: Duration in `timedelta`
        week: `Week` of the lesson

    """

    name: str
    """Lesson's name"""
    teacher: str
    """Teacher's full name."""
    classroom: Optional[str]
    """ Class' room."""
    info: Optional[str]
    """Additional info attached to a unit. """
    start: datetime
    """Full `datetime` of lesson's start time"""
    duration: timedelta
    """Lesson's duration"""

    def __lt__(self, other):
        """Compares dates of units"""
        if not isinstance(other, LessonUnit):
            return NotImplemented
        return self.start < other.start

    def __eq__(self, other):
        """Compares dates of units"""
        if not isinstance(other, LessonUnit):
            return NotImplemented
        return self.start == other.start

    @cached_property
    def week(self):
        """`Week` of the lesson"""
        return Week(self.start)

Class variables

var classroom : Optional[str]

Class' room.

var duration : datetime.timedelta

Lesson's duration

var info : Optional[str]

Additional info attached to a unit.

var name : str

Lesson's name

var start : datetime.datetime

Full datetime of lesson's start time

var teacher : str

Teacher's full name.

Instance variables

var week

Week of the lesson

Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val
class Week (start: datetime.datetime)

Container for a week.

Use it with str() to get the YYYY-MM-DD_YYYY-MM-DD representation used in librus timetable requests.

Attributes

start
Any day in a week. Will be automatically changed to a Monday.
end
End of the week.
Expand source code
class Week:
    """Container for a week.

    Use it with str() to get the YYYY-MM-DD_YYYY-MM-DD representation
    used in librus timetable requests.

    Attributes:
        start: Any day in a week. Will be automatically changed to a Monday.
        end: End of the week.
    """

    def __init__(self, start: datetime):
        if start.weekday() != 0:
            start = start - timedelta(days=start.weekday())
        self.start = start
        """Start of the week."""

    @cached_property
    def end(self) -> datetime:
        """End of the week."""
        return self.start + timedelta(days=6)

    @cached_property
    def ___str__(self):
        fmt = "%Y-%m-%d"
        return self.start.strftime(fmt) + "_" + self.end.strftime(fmt)

    def __str__(self):
        return self.___str__

Instance variables

var end

End of the week.

Expand source code
def __get__(self, instance, owner=None):
    if instance is None:
        return self
    if self.attrname is None:
        raise TypeError(
            "Cannot use cached_property instance without calling __set_name__ on it.")
    try:
        cache = instance.__dict__
    except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
        msg = (
            f"No '__dict__' attribute on {type(instance).__name__!r} "
            f"instance to cache {self.attrname!r} property."
        )
        raise TypeError(msg) from None
    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        with self.lock:
            # check if another thread filled cache while we awaited lock
            val = cache.get(self.attrname, _NOT_FOUND)
            if val is _NOT_FOUND:
                val = self.func(instance)
                try:
                    cache[self.attrname] = val
                except TypeError:
                    msg = (
                        f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                        f"does not support item assignment for caching {self.attrname!r} property."
                    )
                    raise TypeError(msg) from None
    return val
var start

Start of the week.