From 7d4973b50816f65a901163ebd3eb3b9f86753909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 17:17:43 +0200 Subject: [PATCH 01/29] Small changes and added institution check --- README.md | 6 ++++-- docs/conf.py | 2 +- lectio/exceptions.py | 7 +++++++ lectio/lectio.py | 8 ++++++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0d29296..d4a5afd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # lectio.py -![](https://img.shields.io/github/license/dnorhoj/lectio.py) - +[![License](https://img.shields.io/github/license/dnorhoj/lectio.py)](LICENSE) +[![Documentation Status](https://readthedocs.org/projects/lectiopy/badge/?version=latest)](https://lectiopy.readthedocs.io/en/latest/?badge=latest) +[![PyPi version](https://img.shields.io/pypi/v/lectio.py.svg)](https://pypi.org/project/lectio.py/) +[![PyPi supported python versions](https://img.shields.io/pypi/pyversions/lectio.py.svg)](https://python.org/) *Please note: This library is nowhere close to done.* diff --git a/docs/conf.py b/docs/conf.py index 662a94a..0005627 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'dnorhoj' # The full version, including alpha/beta/rc tags -release = '0.0.1' +release = '0.1.1' # -- General configuration --------------------------------------------------- diff --git a/lectio/exceptions.py b/lectio/exceptions.py index 1181afa..744eed6 100644 --- a/lectio/exceptions.py +++ b/lectio/exceptions.py @@ -1,3 +1,6 @@ +"""Here are all the custom Lectio.py exceptions as well as their explanation""" + + class LectioError(Exception): """Base lectio.py exception""" @@ -8,3 +11,7 @@ class UnauthenticatedError(LectioError): class IncorrectCredentialsError(LectioError): """Incorrect credentials error, mostly thrown in auto-login on session expired""" + + +class InstitutionDoesNotExistError(LectioError): + """The institution with the id you provided does not exist.""" diff --git a/lectio/lectio.py b/lectio/lectio.py index cbd68c5..2ffcaec 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -70,7 +70,6 @@ def __init__(self, inst_id: int) -> None: self.__CREDS = [] self.__session = requests.Session() - # TODO: Check if inst_id is valid def authenticate(self, username: str, password: str, save_creds: bool = True) -> bool: """Authenticates you on Lectio. @@ -87,7 +86,8 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> save_creds (bool): Whether the credentials should be saved in the object (useful for auto relogin on logout) Raises: - lectio.IncorrectCredentialsError: When incorrect credentials passed + :class:`exceptions.IncorrectCredentialsError`: When incorrect credentials passed + :class:`exceptions.InstitutionDoesNotExistError`: When the institution id passed on creation of object is invalid Example:: @@ -124,6 +124,10 @@ def _authenticate(self, username: str = None, password: str = None) -> bool: login_page = self.__session.get(self.__BASE_URL + "/login.aspx") + if 'fejlhandled.aspx?title=Skolen+eksisterer+ikke' in login_page.url: + raise exceptions.InstitutionDoesNotExistError(f"The institution with the id '{self._INST_ID}' does not exist!") + + if login_page.status_code != 200: return False From 8aee16690ba98acf64cd1eb3ec2704d5af9f1c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 17:19:27 +0200 Subject: [PATCH 02/29] Added repr to Module --- lectio/lectio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lectio/lectio.py b/lectio/lectio.py index 2ffcaec..7debdf1 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -35,6 +35,9 @@ def __init__(self, **kwargs) -> None: self.end_time = kwargs.get("end_time") self.status = kwargs.get("status") self.url = kwargs.get("url") + + def __repr__(self) -> str: + return f"Module({self.subject}, {self.start_time}, {self.end_time})" def display(self): print(f"Title: {self.title}") From 0b8f55a7e8e81505986c2179ecd0d1010fa9c877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 18:50:00 +0200 Subject: [PATCH 03/29] First prototype/idea of 0.2.0 - Docs not updated --- README.md | 6 +- lectio/__init__.py | 3 +- lectio/exceptions.py | 3 + lectio/helpers/__init__.py | 1 + lectio/helpers/schedule.py | 175 +++++++++++++++++++++++++++++++ lectio/lectio.py | 205 ++++++++----------------------------- lectio/user.py | 35 +++++++ 7 files changed, 263 insertions(+), 165 deletions(-) create mode 100644 lectio/helpers/__init__.py create mode 100644 lectio/helpers/schedule.py create mode 100644 lectio/user.py diff --git a/README.md b/README.md index d4a5afd..27d3356 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ To use this repository feel free to check the documentation [here](https://lecti * [ ] Make a better README * [ ] Start doing the hard part (basically everything) +## Known bugs + +* Not made to work with teacher accounts (as i have no way of testing anything) + ## Contributing If you want to contribute, you can just fork this repository and start a pull request. @@ -32,4 +36,4 @@ If you notice something that isn't working as intended you can start an issue. ## License -This repository uses the `GNU Lesser General Public License v3.0` you can read more about it in [LICENSE](LICENSE). \ No newline at end of file +This repository uses the `GNU Lesser General Public License v3.0` you can read more about it in [LICENSE](LICENSE). diff --git a/lectio/__init__.py b/lectio/__init__.py index 8709e99..254284c 100644 --- a/lectio/__init__.py +++ b/lectio/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa -from .lectio import Lectio, Module +from .lectio import Lectio +from . import user from . import lectio from . import exceptions diff --git a/lectio/exceptions.py b/lectio/exceptions.py index 744eed6..e73371c 100644 --- a/lectio/exceptions.py +++ b/lectio/exceptions.py @@ -15,3 +15,6 @@ class IncorrectCredentialsError(LectioError): class InstitutionDoesNotExistError(LectioError): """The institution with the id you provided does not exist.""" + +class UserDoesNotExistError(LectioError): + """The user does not exist.""" \ No newline at end of file diff --git a/lectio/helpers/__init__.py b/lectio/helpers/__init__.py new file mode 100644 index 0000000..8ea1b7e --- /dev/null +++ b/lectio/helpers/__init__.py @@ -0,0 +1 @@ +from .schedule import * \ No newline at end of file diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py new file mode 100644 index 0000000..4e0d8c5 --- /dev/null +++ b/lectio/helpers/schedule.py @@ -0,0 +1,175 @@ +import re +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, List +from bs4 import BeautifulSoup + +if TYPE_CHECKING: + from ..lectio import Lectio + + +class Module: + """Lectio module object + + Represents a lectio module + + Args: + title (str|None): Optional description of module (not present in all modules) + subject (str|None): "Hold" from lectio, bascially which subject. + Example: `1.a Da` + teacher (str|None): Initials of teacher. + Example: `abcd` + room (str|None): Room name of module. + Example: `0.015` + extra_info (str|None): Extra info from module, includes homework and other info. + start_time (:class:`datetime.datetime`): Start time of module + end_time (:class:`datetime.datetime`): End time of module + status (int): 0=normal, 1=changed, 2=cancelled + url (str|None): Url for more info for the module + """ + + def __init__(self, **kwargs) -> None: + self.title = kwargs.get("title") + self.subject = kwargs.get("subject") + self.teacher = kwargs.get("teacher") + self.room = kwargs.get("room") + self.extra_info = kwargs.get("extra_info") + self.start_time = kwargs.get("start_time") + self.end_time = kwargs.get("end_time") + self.status = kwargs.get("status") + self.url = kwargs.get("url") + + def __repr__(self) -> str: + return f"Module({self.subject}, {self.start_time}, {self.end_time})" + + def display(self): + print(f"Title: {self.title}") + print(f"Subject(s): {self.subject}") + print(f"Teacher(s): {self.teacher}") + print(f"Room(s): {self.room}") + print(f"Starts at: {self.start_time}") + print(f"Ends at: {self.end_time}") + print(f"Status: {self.status}") + print(f"URL: {self.url}") + print(f"Extra info:\n\n{self.extra_info}") + + +def get_schedule(lectio: 'Lectio', params: List[str], start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: + """Get lectio schedule for current or specific week. + + Get all modules in specified time range. + + Parameters: + lectio (:class:`lectio.Lectio`): Base lectio object + params (list): List of get parameters to add to request + start_date (:class:`datetime.datetime`): Start date + end_date (:class:`datetime.datetime`): End date + strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time. + Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date. + + Returns: + List[:class:`lectio.Module`]: List containing all modules in specified time range. + """ + + replacetime = {} + if strip_time: + end_date = end_date + timedelta(days=1) + replacetime = { + "hour": 0, + "minute": 0, + "second": 0, + } + + start_date = start_date.replace( + **replacetime, microsecond=0).isoformat() + end_date = end_date.replace(**replacetime, microsecond=0).isoformat() + + params = "&".join([ + "type=ShowListAll", + f"starttime={start_date}", + f"endtime={end_date}", + "dagsbemaerk=0", + *params + ]) + + schedule_request = lectio._request( + f"{lectio._BASE_URL}/SkemaAvanceret.aspx?{params}") + + soup = BeautifulSoup(schedule_request.text, 'html.parser') + + modules = soup.find( + "table", class_="list texttop lf-grid").findChildren('tr', class_=None) + + schedule = [] + for module in modules: + a = module.findChild('a') + module = parse_additionalinfo( + a.attrs.get('data-additionalinfo')) + + # Add href to module + href = a.attrs.get('href') + if href: + module.url = f"https://www.lectio.dk{href}" + + schedule.append(module) + + return schedule + + +def parse_additionalinfo(info: str) -> Module: + module = Module() + + info_list = info.split('\n') + + # Parse module status + if info_list[0] == 'Ændret!': + module.status = 1 + info_list.pop(0) + elif info_list[0] == 'Aflyst!': + module.status = 2 + info_list.pop(0) + else: + module.status = 0 + + # Parse title + if not re.match(r'^[0-9]{1,2}\/[0-9]{1,2}-[0-9]{4} [0-9]{2}:[0-9]{2}', info_list[0]): + module.title = info_list[0] + info_list.pop(0) + + # Parse time + times = info_list[0].split(" til ") + info_list.pop(0) + module.start_time = datetime.strptime(times[0], "%d/%m-%Y %H:%M") + if len(times[1]) == 5: + module.end_time = datetime.strptime( + times[0][:-5] + times[1], "%d/%m-%Y %H:%M") + else: + module.end_time = datetime.strptime(times[1], "%d/%m-%Y %H:%M") + + # Parse subject(s) + subject = re.search(r"Hold: (.*)", info) + if subject: + info_list.pop(0) + module.subject = subject[1] + + # Parse teacher(s) + teacher = re.search(r"Lærere?: (.*)", info) + if teacher: + info_list.pop(0) + module.teacher = teacher[1] + + # Parse room(s) + room = re.search(r"Lokaler?: (.*)", info) + if room: + info_list.pop(0) + module.room = room[1] + + # Put any additional info into extra_info + if info_list: + info_list.pop(0) + module.extra_info = "\n".join(info_list) + + return module + + +def get_sched_for_student(lectio: 'Lectio', student_id: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: + return get_schedule(lectio, [f"studentsel={student_id}"], start_date, end_date, strip_time) diff --git a/lectio/lectio.py b/lectio/lectio.py index 7debdf1..990a626 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -1,54 +1,12 @@ from datetime import datetime, timedelta import re +from typing import Union import requests from bs4 import BeautifulSoup -from . import exceptions - - -class Module: - """Lectio module object - - Represents a lectio module - Args: - title (str|None): Optional description of module (not present in all modules) - subject (str|None): "Hold" from lectio, bascially which subject. - Example: `1.a Da` - teacher (str|None): Initials of teacher. - Example: `abcd` - room (str|None): Room name of module. - Example: `0.015` - extra_info (str|None): Extra info from module, includes homework and other info. - start_time (:class:`datetime.datetime`): Start time of module - end_time (:class:`datetime.datetime`): End time of module - status (int): 0=normal, 1=changed, 2=cancelled - url (str|None): Url for more info for the module - """ +from . import exceptions - def __init__(self, **kwargs) -> None: - self.title = kwargs.get("title") - self.subject = kwargs.get("subject") - self.teacher = kwargs.get("teacher") - self.room = kwargs.get("room") - self.extra_info = kwargs.get("extra_info") - self.start_time = kwargs.get("start_time") - self.end_time = kwargs.get("end_time") - self.status = kwargs.get("status") - self.url = kwargs.get("url") - - def __repr__(self) -> str: - return f"Module({self.subject}, {self.start_time}, {self.end_time})" - - def display(self): - print(f"Title: {self.title}") - print(f"Subject(s): {self.subject}") - print(f"Teacher(s): {self.teacher}") - print(f"Room(s): {self.room}") - print(f"Starts at: {self.start_time}") - print(f"Ends at: {self.end_time}") - print(f"Status: {self.status}") - print(f"URL: {self.url}") - print(f"Extra info:\n\n{self.extra_info}") +from .user import Student, Teacher, Types as UserType class Lectio: @@ -68,11 +26,12 @@ class Lectio: """ def __init__(self, inst_id: int) -> None: - self.__INST_ID = inst_id - self.__BASE_URL = f"https://www.lectio.dk/lectio/{str(inst_id)}" self.__CREDS = [] - self.__session = requests.Session() + + self.inst_id = inst_id + self._BASE_URL = f"https://www.lectio.dk/lectio/{str(inst_id)}" + def authenticate(self, username: str, password: str, save_creds: bool = True) -> bool: """Authenticates you on Lectio. @@ -113,7 +72,7 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> self._authenticate(username, password) # Check if authentication passed - self._request(self.__BASE_URL + "/forside.aspx") + self._request(self._BASE_URL + "/forside.aspx") def _authenticate(self, username: str = None, password: str = None) -> bool: if username is None or password is None: @@ -125,11 +84,11 @@ def _authenticate(self, username: str = None, password: str = None) -> bool: self.log_out() # Clear session - login_page = self.__session.get(self.__BASE_URL + "/login.aspx") + login_page = self.__session.get(self._BASE_URL + "/login.aspx") if 'fejlhandled.aspx?title=Skolen+eksisterer+ikke' in login_page.url: - raise exceptions.InstitutionDoesNotExistError(f"The institution with the id '{self._INST_ID}' does not exist!") - + raise exceptions.InstitutionDoesNotExistError( + f"The institution with the id '{self._INST_ID}' does not exist!") if login_page.status_code != 200: return False @@ -137,7 +96,7 @@ def _authenticate(self, username: str = None, password: str = None) -> bool: parser = BeautifulSoup(login_page.text, "html.parser") self.__session.post( - self.__BASE_URL + "/login.aspx", + self._BASE_URL + "/login.aspx", data={ "time": 0, "__EVENTTARGET": "m$Content$submitbtn2", @@ -152,144 +111,64 @@ def _authenticate(self, username: str = None, password: str = None) -> bool: } ) - def get_user_id(self) -> int: - """Gets your user id + def me(self) -> Student: + """Gets own student object Returns: - int: User id + :class:`lectio.profile.User`: User object """ - r = self._request(self.__BASE_URL + "/forside.aspx") + r = self._request(self._BASE_URL + "/forside.aspx") soup = BeautifulSoup(r.text, 'html.parser') content = soup.find( 'meta', {'name': 'msapplication-starturl'}).attrs.get('content') - user_id = int(re.match(r'.*id=([0-9]+)$', content)[1]) - - return user_id + user_id = re.match(r'.*id=([0-9]+)$', content)[1] - def get_schedule_for_student(self, elevid: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> any: - """Get lectio schedule for current or specific week. + return self.get_user(user_id, check=False) - Get all modules in specified time range. + def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> Union[Student, Teacher]: + """Gets a user by their id - Parameters: - elevid (int): Student id - start_date (:class:`datetime.datetime`): Start date - end_date (:class:`datetime.datetime`): End date - strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time. - Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date. + Args: + user_id (str): The id of the user + user_type (int): The type of the user (student or teacher) + check (bool): Whether to check if the user exists Returns: - list: List containing all modules in specified time range. - """ - - replacetime = {} - if strip_time: - end_date = end_date + timedelta(days=1) - replacetime = { - "hour": 0, - "minute": 0, - "second": 0, - } - - start_date = start_date.replace( - **replacetime, microsecond=0).isoformat() - end_date = end_date.replace(**replacetime, microsecond=0).isoformat() - - params = ( - "type=ShowListAll&" - f"starttime={start_date}&" - f"endtime={end_date}&" - "dagsbemaerk=0&" - f"studentsel={elevid}" - ) + :class:`lectio.profile.User`: User object - schedule_request = self._request( - f"{self.__BASE_URL}/SkemaAvanceret.aspx?{params}") - - soup = BeautifulSoup(schedule_request.text, 'html.parser') - - modules = soup.find( - "table", class_="list texttop lf-grid").findChildren('tr', class_=None) + Raises: + :class:`lectio.exceptions.UserDoesNotExistError`: When the user does not exist + """ - schedule = [] - for module in modules: - a = module.findChild('a') - module = self._parse_additionalinfo( - a.attrs.get('data-additionalinfo')) - - href = a.attrs.get('href') - if href is not None: - module.url = f"https://www.lectio.dk{href}" - schedule.append(module) + if check: + type_str = "elev" if user_type == UserType.STUDENT else "laerer" - return schedule + # Check if user exists + r = self._request( + f"{self._BASE_URL}/SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") - def _parse_additionalinfo(self, info: str) -> Module: - module = Module() + soup = BeautifulSoup(r.text, 'html.parser') - info_list = info.split('\n') + if soup.title.string.strip().startswith("Fejl - Lectio"): + raise exceptions.UserDoesNotExistError( + f"The user with the id '{user_id}' does not exist!") - # Parse module status - if info_list[0] == 'Ændret!': - module.status = 1 - info_list.pop(0) - elif info_list[0] == 'Aflyst!': - module.status = 2 - info_list.pop(0) - else: - module.status = 0 - - # Parse title - if not re.match(r'^[0-9]{1,2}\/[0-9]{1,2}-[0-9]{4} [0-9]{2}:[0-9]{2}', info_list[0]): - module.title = info_list[0] - info_list.pop(0) - - # Parse time - times = info_list[0].split(" til ") - info_list.pop(0) - module.start_time = datetime.strptime(times[0], "%d/%m-%Y %H:%M") - if len(times[1]) == 5: - module.end_time = datetime.strptime( - times[0][:-5] + times[1], "%d/%m-%Y %H:%M") + if user_type == UserType.TEACHER: + return Teacher(self, user_id) else: - module.end_time = datetime.strptime(times[1], "%d/%m-%Y %H:%M") - - # Parse subject(s) - subject = re.search(r"Hold: (.*)", info) - if subject: - info_list.pop(0) - module.subject = subject[1] - - # Parse teacher(s) - teacher = re.search(r"Lærere?: (.*)", info) - if teacher: - info_list.pop(0) - module.teacher = teacher[1] - - # Parse room(s) - room = re.search(r"Lokaler?: (.*)", info) - if room: - info_list.pop(0) - module.room = room[1] - - # Put any additional info into extra_info - if info_list: - info_list.pop(0) - module.extra_info = "\n".join(info_list) - - return module + return Student(self, user_id) def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: r = self.__session.request(method, url, **kwargs) - if f"{self.__INST_ID}/login.aspx?prevurl=" in r.url: + if f"{self.inst_id}/login.aspx?prevurl=" in r.url: self._authenticate() r = self.__session.get(url) - if f"{self.__INST_ID}/login.aspx?prevurl=" in r.url: + if f"{self.inst_id}/login.aspx?prevurl=" in r.url: raise exceptions.IncorrectCredentialsError( "Could not restore session, probably incorrect credentials") diff --git a/lectio/user.py b/lectio/user.py new file mode 100644 index 0000000..871ae70 --- /dev/null +++ b/lectio/user.py @@ -0,0 +1,35 @@ +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, List + +from bs4 import BeautifulSoup + +from .helpers.schedule import get_sched_for_student + +if TYPE_CHECKING: + from .helpers.schedule import Module + from .lectio import Lectio + +class Types: + STUDENT = 0 + TEACHER = 1 + +class UserMixin: + def __init__(self, lectio: 'Lectio', user_id: int): + self._lectio = lectio + self.user_id = user_id + +class Student(UserMixin): + def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: + return get_sched_for_student(self._lectio, self.user_id, start_date, end_date, strip_time) + + def __repr__(self) -> str: + return f"Student({self.user_id})" + +class Teacher(UserMixin): + def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: + # TODO + #get_sched_for_teacher(self._lectio, self.user_id, start_date, end_date, strip_time) + pass + + def __repr__(self) -> str: + return f"Teacher({self.user_id})" From 4ab533b79601e49cbc12a8e26e89620df39e07e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 22:44:09 +0200 Subject: [PATCH 04/29] Refactoring and authentication raises correct error on incorrect creds --- lectio/exceptions.py | 3 +- lectio/helpers/__init__.py | 2 +- lectio/helpers/schedule.py | 3 +- lectio/lectio.py | 67 +++++++++++++++++++------------------- lectio/user.py | 12 ++++++- 5 files changed, 48 insertions(+), 39 deletions(-) diff --git a/lectio/exceptions.py b/lectio/exceptions.py index e73371c..07b8990 100644 --- a/lectio/exceptions.py +++ b/lectio/exceptions.py @@ -16,5 +16,6 @@ class IncorrectCredentialsError(LectioError): class InstitutionDoesNotExistError(LectioError): """The institution with the id you provided does not exist.""" + class UserDoesNotExistError(LectioError): - """The user does not exist.""" \ No newline at end of file + """The user does not exist.""" diff --git a/lectio/helpers/__init__.py b/lectio/helpers/__init__.py index 8ea1b7e..b7ee570 100644 --- a/lectio/helpers/__init__.py +++ b/lectio/helpers/__init__.py @@ -1 +1 @@ -from .schedule import * \ No newline at end of file +from .schedule import * diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py index 4e0d8c5..283811f 100644 --- a/lectio/helpers/schedule.py +++ b/lectio/helpers/schedule.py @@ -91,8 +91,7 @@ def get_schedule(lectio: 'Lectio', params: List[str], start_date: datetime, end_ *params ]) - schedule_request = lectio._request( - f"{lectio._BASE_URL}/SkemaAvanceret.aspx?{params}") + schedule_request = lectio._request(f"SkemaAvanceret.aspx?{params}") soup = BeautifulSoup(schedule_request.text, 'html.parser') diff --git a/lectio/lectio.py b/lectio/lectio.py index 990a626..bdbbfe5 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -28,10 +28,8 @@ class Lectio: def __init__(self, inst_id: int) -> None: self.__CREDS = [] self.__session = requests.Session() - - self.inst_id = inst_id - self._BASE_URL = f"https://www.lectio.dk/lectio/{str(inst_id)}" + self.inst_id = inst_id def authenticate(self, username: str, password: str, save_creds: bool = True) -> bool: """Authenticates you on Lectio. @@ -53,14 +51,14 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> Example:: - from lectio import Lectio, errors + from lectio import Lectio, exceptions lect = Lectio(123) try: lect.authenticate("username", "password"): print("Authenticated") - except errors.IncorrectCredentialsError: + except exceptions.IncorrectCredentialsError: print("Not authenticated") """ @@ -71,45 +69,43 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> # Call the actual authentication method self._authenticate(username, password) - # Check if authentication passed - self._request(self._BASE_URL + "/forside.aspx") - - def _authenticate(self, username: str = None, password: str = None) -> bool: + def _authenticate(self, username: str = None, password: str = None): if username is None or password is None: if len(self.__CREDS) != 2: raise exceptions.UnauthenticatedError( - "Auto auth failed, did you authenticate?") + "No authentication details provided and no saved credentials found!") username, password = self.__CREDS self.log_out() # Clear session - login_page = self.__session.get(self._BASE_URL + "/login.aspx") + URL = f"https://www.lectio.dk/lectio/{self.inst_id}/login.aspx" + + login_page = self.__session.get(URL) if 'fejlhandled.aspx?title=Skolen+eksisterer+ikke' in login_page.url: raise exceptions.InstitutionDoesNotExistError( f"The institution with the id '{self._INST_ID}' does not exist!") - if login_page.status_code != 200: - return False - parser = BeautifulSoup(login_page.text, "html.parser") - self.__session.post( - self._BASE_URL + "/login.aspx", - data={ - "time": 0, - "__EVENTTARGET": "m$Content$submitbtn2", - "__EVENTARGUMENT": "", - "__SCROLLPOSITION": "", - "__VIEWSTATEX": parser.find(attrs={"name": "__VIEWSTATEX"})["value"], - "__VIEWSTATEY_KEY": "", - "__VIEWSTATE": "", - "__EVENTVALIDATION": parser.find(attrs={"name": "__EVENTVALIDATION"})["value"], - "m$Content$username": username, - "m$Content$password": password - } - ) + r = self.__session.post(URL, data={ + "time": 0, + "__EVENTTARGET": "m$Content$submitbtn2", + "__EVENTARGUMENT": "", + "__SCROLLPOSITION": "", + "__VIEWSTATEX": parser.find(attrs={"name": "__VIEWSTATEX"})["value"], + "__VIEWSTATEY_KEY": "", + "__VIEWSTATE": "", + "__EVENTVALIDATION": parser.find(attrs={"name": "__EVENTVALIDATION"})["value"], + "m$Content$username": username, + "m$Content$password": password + }) + + if r.url == URL: + # Authentication failed + raise exceptions.IncorrectCredentialsError( + "Incorrect credentials provided!") def me(self) -> Student: """Gets own student object @@ -118,7 +114,7 @@ def me(self) -> Student: :class:`lectio.profile.User`: User object """ - r = self._request(self._BASE_URL + "/forside.aspx") + r = self._request("forside.aspx") soup = BeautifulSoup(r.text, 'html.parser') @@ -149,7 +145,7 @@ def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool # Check if user exists r = self._request( - f"{self._BASE_URL}/SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") + f"SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") soup = BeautifulSoup(r.text, 'html.parser') @@ -163,11 +159,14 @@ def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool return Student(self, user_id) def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: - r = self.__session.request(method, url, **kwargs) + r = self.__session.request( + method, f"https://www.lectio.dk/lectio/{str(self.inst_id)}/{url}", **kwargs) if f"{self.inst_id}/login.aspx?prevurl=" in r.url: - self._authenticate() - r = self.__session.get(url) + if not self._authenticate(): + raise exceptions.UnauthenticatedError("Unauthenticated") + r = self.__session.get( + f"https://www.lectio.dk/lectio/{str(self.inst_id)}/{url}") if f"{self.inst_id}/login.aspx?prevurl=" in r.url: raise exceptions.IncorrectCredentialsError( "Could not restore session, probably incorrect credentials") diff --git a/lectio/user.py b/lectio/user.py index 871ae70..bf23073 100644 --- a/lectio/user.py +++ b/lectio/user.py @@ -9,22 +9,32 @@ from .helpers.schedule import Module from .lectio import Lectio + class Types: STUDENT = 0 TEACHER = 1 + class UserMixin: + name = None + def __init__(self, lectio: 'Lectio', user_id: int): self._lectio = lectio self.user_id = user_id + class Student(UserMixin): + def get_name(self): + # TODO + #return self._lectio._request("") + def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: return get_sched_for_student(self._lectio, self.user_id, start_date, end_date, strip_time) - + def __repr__(self) -> str: return f"Student({self.user_id})" + class Teacher(UserMixin): def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: # TODO From b12e8fd6a250b74df784f69e60ea8e97203c30a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 23:40:19 +0200 Subject: [PATCH 05/29] Added progress to README --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27d3356..9d1b677 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,26 @@ You can read the documentation To use this repository feel free to check the documentation [here](https://lectiopy.rtfd.io/). +## Progress + +
+ Implementation progress + +* [x] Schedule +* [x] User info +* [ ] Absence +* [ ] Mail +* [ ] Assignments +* [ ] Homework +* [ ] Surveys (Probably not going to be implemented) +* [ ] Grades +* [ ] Search for students / teachers + +
+ ## Todo -* [ ] Finish authentication check * [ ] Make a better README -* [ ] Start doing the hard part (basically everything) ## Known bugs From 24c3e69c5cdc206c0662ffaa09755e21bcbca976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 23:42:45 +0200 Subject: [PATCH 06/29] A lot of progress and user class is complete for now --- lectio/helpers/schedule.py | 3 + lectio/lectio.py | 15 ++--- lectio/user.py | 114 +++++++++++++++++++++++++++++-------- 3 files changed, 99 insertions(+), 33 deletions(-) diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py index 283811f..3b57bc8 100644 --- a/lectio/helpers/schedule.py +++ b/lectio/helpers/schedule.py @@ -172,3 +172,6 @@ def parse_additionalinfo(info: str) -> Module: def get_sched_for_student(lectio: 'Lectio', student_id: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: return get_schedule(lectio, [f"studentsel={student_id}"], start_date, end_date, strip_time) + +def get_sched_for_teacher(lectio: 'Lectio', teacher_id: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: + return get_schedule(lectio, [f"teachersel={teacher_id}"], start_date, end_date, strip_time) \ No newline at end of file diff --git a/lectio/lectio.py b/lectio/lectio.py index bdbbfe5..621c57c 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -1,12 +1,10 @@ -from datetime import datetime, timedelta import re -from typing import Union import requests from bs4 import BeautifulSoup from . import exceptions -from .user import Student, Teacher, Types as UserType +from .user import User, UserType class Lectio: @@ -107,8 +105,8 @@ def _authenticate(self, username: str = None, password: str = None): raise exceptions.IncorrectCredentialsError( "Incorrect credentials provided!") - def me(self) -> Student: - """Gets own student object + def me(self) -> User: + """Gets own user object Returns: :class:`lectio.profile.User`: User object @@ -125,7 +123,7 @@ def me(self) -> Student: return self.get_user(user_id, check=False) - def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> Union[Student, Teacher]: + def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> User: """Gets a user by their id Args: @@ -153,10 +151,7 @@ def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool raise exceptions.UserDoesNotExistError( f"The user with the id '{user_id}' does not exist!") - if user_type == UserType.TEACHER: - return Teacher(self, user_id) - else: - return Student(self, user_id) + return User(self, user_id, user_type) def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: r = self.__session.request( diff --git a/lectio/user.py b/lectio/user.py index bf23073..2e9ef4d 100644 --- a/lectio/user.py +++ b/lectio/user.py @@ -1,45 +1,113 @@ -from datetime import datetime, timedelta -from typing import TYPE_CHECKING, List - from bs4 import BeautifulSoup +from typing import TYPE_CHECKING, List -from .helpers.schedule import get_sched_for_student +from .helpers.schedule import get_sched_for_student, get_sched_for_teacher if TYPE_CHECKING: + from datetime import datetime from .helpers.schedule import Module from .lectio import Lectio -class Types: +class UserType: + """User types + + Attributes: + STUDENT (int): Student + TEACHER (int): Teacher + + Example: + >>> from lectio import Lectio + >>> from lectio.user import UserType + >>> lec = Lectio(123) + >>> lec.authenticate("username", "password") + >>> me = lec.me() + >>> print(me.type) + 0 + >>> print(me.type == UserType.STUDENT) + True + """ + STUDENT = 0 TEACHER = 1 -class UserMixin: - name = None +class User: + """Lectio user object + + Represents a lectio user + + Args: + lectio (:class:`lectio.Lectio`): Lectio object + user_id (int): User id + user_type (int): User type (UserType.STUDENT or UserType.TEACHER) - def __init__(self, lectio: 'Lectio', user_id: int): + Attributes: + id (int): User id + type (int): User type (UserType.STUDENT or UserType.TEACHER) + name (str): Name of user + initials (str|None): Initials of user if user is a teacher + class_name (str|None): Class of user if user is a student + image (str): User image url + """ + + def __init__(self, lectio: 'Lectio', user_id: int, user_type: int = UserType.STUDENT) -> None: self._lectio = lectio - self.user_id = user_id + self.id = user_id + if not user_type in [UserType.STUDENT, UserType.TEACHER]: + raise ValueError("Invalid user type") -class Student(UserMixin): - def get_name(self): - # TODO - #return self._lectio._request("") + self.type = user_type + self.__populate() - def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: - return get_sched_for_student(self._lectio, self.user_id, start_date, end_date, strip_time) + # TODO; Don't know if user exist check should be here or in lectio.py - def __repr__(self) -> str: - return f"Student({self.user_id})" + def __populate(self) -> None: + """Populate user object + + Populates the user object with data from lectio, such as name, class name, etc. + """ + + params = "" + if self.type == UserType.STUDENT: + params = f"type=elev&elevid={self.id}" + elif self.type == UserType.TEACHER: + params = f"type=laerer&laererid={self.id}" + + r = self._lectio._request(f"SkemaNy.aspx?{params}") + + soup = BeautifulSoup(r.text, "html.parser") + + title = soup.find("div", {"id": "s_m_HeaderContent_MainTitle"}).text + + title = " ".join(title.split()[1:]) + + if self.type == UserType.STUDENT: + self.name = title.split(", ")[0] + self.class_name = title.split(", ")[1].split(" - ")[0] + elif self.type == UserType.TEACHER: + self.initials, self.name, *_ = title.split(" - ") + + src = soup.find( + "img", {"id": "s_m_HeaderContent_picctrlthumbimage"}).get("src") + + self.image = f"https://www.lectio.dk{src}&fullsize=1" + + def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']: + """Get schedule for user + Args: + start_date (:class:`datetime.datetime`): Start date + end_date (:class:`datetime.datetime`): End date + strip_time (bool): Strip time from datetime objects (default: True) + """ -class Teacher(UserMixin): - def get_schedule(self, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List['Module']: - # TODO - #get_sched_for_teacher(self._lectio, self.user_id, start_date, end_date, strip_time) - pass + if self.type == UserType.STUDENT: + return get_sched_for_student(self._lectio, self.id, start_date, end_date, strip_time) + elif self.type == UserType.TEACHER: + return get_sched_for_teacher(self._lectio, self.id, start_date, end_date, strip_time) def __repr__(self) -> str: - return f"Teacher({self.user_id})" + type_str = "Student" if self.type == UserType.STUDENT else "Teacher" + return f"User({type_str}, {self.id})" From 7c3fff71be7fd5bca882a80df5922eba49930731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 23:48:52 +0200 Subject: [PATCH 07/29] Added install instructions to README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d1b677..de05fad 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,11 @@ You can read the documentation ## How do i use this? -To use this repository feel free to check the documentation [here](https://lectiopy.rtfd.io/). +You can install this library via `pip`: + + pip install lectio.py + +For a quickstart guide as well as some examples, you can read the documentation [here](https://lectiopy.rtfd.io/). ## Progress @@ -38,6 +42,7 @@ To use this repository feel free to check the documentation [here](https://lecti ## Todo * [ ] Make a better README +* [ ] Quickstart, Examples, etc. ## Known bugs From 121b00d6ff8a83ad970a27402b1843212ad673b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Sun, 25 Sep 2022 23:49:06 +0200 Subject: [PATCH 08/29] Bump version to 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 032f5b7..9ed3735 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="lectio.py", - version="0.1.1", + version="0.2.0", author="dnorhoj", author_email="daniel.norhoj@gmail.com", description="Interact with lectio through python", From ea70a6f3f3bf3640ebff268253ae9e472f2dfc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 22:12:56 +0200 Subject: [PATCH 09/29] Structural changes, lazy loading implemented on user class --- lectio/helpers/schedule.py | 7 --- lectio/lectio.py | 9 ++- lectio/school.py | 61 +++++++++++++++++++ lectio/user.py | 121 +++++++++++++++++++++++++++++++------ 4 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 lectio/school.py diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py index 3b57bc8..7d2d30b 100644 --- a/lectio/helpers/schedule.py +++ b/lectio/helpers/schedule.py @@ -168,10 +168,3 @@ def parse_additionalinfo(info: str) -> Module: module.extra_info = "\n".join(info_list) return module - - -def get_sched_for_student(lectio: 'Lectio', student_id: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: - return get_schedule(lectio, [f"studentsel={student_id}"], start_date, end_date, strip_time) - -def get_sched_for_teacher(lectio: 'Lectio', teacher_id: int, start_date: datetime, end_date: datetime, strip_time: bool = True) -> List[Module]: - return get_schedule(lectio, [f"teachersel={teacher_id}"], start_date, end_date, strip_time) \ No newline at end of file diff --git a/lectio/lectio.py b/lectio/lectio.py index 621c57c..d43c171 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -5,6 +5,7 @@ from . import exceptions from .user import User, UserType +from .school import School class Lectio: @@ -54,7 +55,7 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> lect = Lectio(123) try: - lect.authenticate("username", "password"): + lect.authenticate("username", "password") print("Authenticated") except exceptions.IncorrectCredentialsError: print("Not authenticated") @@ -105,6 +106,12 @@ def _authenticate(self, username: str = None, password: str = None): raise exceptions.IncorrectCredentialsError( "Incorrect credentials provided!") + @property + def school(self) -> School: + """:class:`lectio.school.School`: The school object for the authenticated user.""" + + return School(self) + def me(self) -> User: """Gets own user object diff --git a/lectio/school.py b/lectio/school.py new file mode 100644 index 0000000..1938b24 --- /dev/null +++ b/lectio/school.py @@ -0,0 +1,61 @@ +from typing import TYPE_CHECKING, List +from bs4 import BeautifulSoup + +from .user import User + +if TYPE_CHECKING: + from .lectio import Lectio + + +class School: + """A school object. + + Represents a school. + + Note: + This class should not be instantiated directly, + but rather through the :meth:`lectio.Lectio.get_school` method. + + Args: + lectio (:class:`lectio.Lectio`): Lectio object + """ + + def __init__(self, lectio: 'Lectio') -> None: + self._lectio = lectio + self.__populate() + + def __populate(self) -> None: + r = self._lectio._request("forside.aspx") + + soup = BeautifulSoup(r.text, 'html.parser') + + self.name = soup.find( + "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "") + + def get_user_by_letter(self, letter: str) -> List[User]: + """Get students by first letter of name + + Args: + letter (str): Letter to search for + + Returns: + list(:class:`lectio.User`): List of students + """ + + r = self._lectio._request("FindSkema.aspx?type=elev&forbogstav=" + letter) + + soup = BeautifulSoup(r.text, 'html.parser') + + students = [] + + # Container containing all students + lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"}) + + # Iterate and create user objects + for i in lst.find_all("li"): + students.append(User(self._lectio, int(i.a["href"].split("=")[-1]))) + + return students + + def __repr__(self) -> str: + return f"School({self.name})" diff --git a/lectio/user.py b/lectio/user.py index 2e9ef4d..1996e65 100644 --- a/lectio/user.py +++ b/lectio/user.py @@ -1,7 +1,7 @@ from bs4 import BeautifulSoup from typing import TYPE_CHECKING, List -from .helpers.schedule import get_sched_for_student, get_sched_for_teacher +from .helpers.schedule import get_schedule if TYPE_CHECKING: from datetime import datetime @@ -31,37 +31,75 @@ class UserType: STUDENT = 0 TEACHER = 1 + @staticmethod + def get_str(user_type: int, en: bool = False) -> str: + """Get string representation of user type for lectio interface + + Args: + type (int): User type + en (bool): Whether to return english string instead of Danish + + Returns: + str: String representation of user type + """ + + if user_type == UserType.STUDENT: + if en: + return "student" + else: + return "elev" + elif user_type == UserType.TEACHER: + if en: + return "teacher" + else: + return "laerer" + class User: """Lectio user object Represents a lectio user + Note: + This class should not be instantiated directly, + but rather through the :meth:`lectio.Lectio.me` or :meth:`lectio.Lectio.get_user` methods. + Args: lectio (:class:`lectio.Lectio`): Lectio object user_id (int): User id user_type (int): User type (UserType.STUDENT or UserType.TEACHER) + lazy (bool): Whether to not populate user object on instantiation (default: False) Attributes: id (int): User id type (int): User type (UserType.STUDENT or UserType.TEACHER) - name (str): Name of user + name (str): Full name of user initials (str|None): Initials of user if user is a teacher class_name (str|None): Class of user if user is a student image (str): User image url """ - def __init__(self, lectio: 'Lectio', user_id: int, user_type: int = UserType.STUDENT) -> None: + __name = None + __initials = None + __class_name = None + __image = None + + def __init__(self, lectio: 'Lectio', user_id: int, user_type: int = UserType.STUDENT, *, lazy=False, **user_data) -> None: self._lectio = lectio self.id = user_id - if not user_type in [UserType.STUDENT, UserType.TEACHER]: + if user_type not in [UserType.STUDENT, UserType.TEACHER]: raise ValueError("Invalid user type") self.type = user_type - self.__populate() - # TODO; Don't know if user exist check should be here or in lectio.py + if not lazy: + self.__populate() + else: + self.__name = user_data.get("name") + self.__initials = user_data.get("initials") + self.__class_name = user_data.get("class_name") + self.__image = user_data.get("image") def __populate(self) -> None: """Populate user object @@ -69,13 +107,11 @@ def __populate(self) -> None: Populates the user object with data from lectio, such as name, class name, etc. """ - params = "" - if self.type == UserType.STUDENT: - params = f"type=elev&elevid={self.id}" - elif self.type == UserType.TEACHER: - params = f"type=laerer&laererid={self.id}" + # TODO; Check if user is student or teacher - r = self._lectio._request(f"SkemaNy.aspx?{params}") + # Get user's schedule for today + r = self._lectio._request( + f"SkemaNy.aspx?type={UserType.get_str(self.type)}&{UserType.get_str(self.type)}id={self.id}") soup = BeautifulSoup(r.text, "html.parser") @@ -84,15 +120,15 @@ def __populate(self) -> None: title = " ".join(title.split()[1:]) if self.type == UserType.STUDENT: - self.name = title.split(", ")[0] - self.class_name = title.split(", ")[1].split(" - ")[0] + self.__name = title.split(", ")[0] + self.__class_name = title.split(", ")[1].split(" - ")[0] elif self.type == UserType.TEACHER: - self.initials, self.name, *_ = title.split(" - ") + self.__initials, self.__name, *_ = title.split(" - ") src = soup.find( "img", {"id": "s_m_HeaderContent_picctrlthumbimage"}).get("src") - self.image = f"https://www.lectio.dk{src}&fullsize=1" + self.__image = f"https://www.lectio.dk{src}&fullsize=1" def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']: """Get schedule for user @@ -103,11 +139,56 @@ def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: strip_time (bool): Strip time from datetime objects (default: True) """ - if self.type == UserType.STUDENT: - return get_sched_for_student(self._lectio, self.id, start_date, end_date, strip_time) - elif self.type == UserType.TEACHER: - return get_sched_for_teacher(self._lectio, self.id, start_date, end_date, strip_time) + return get_schedule( + self._lectio, + [f"{UserType.get_str(self.type, True)}sel={self.id}"], + start_date, + end_date, + strip_time + ) def __repr__(self) -> str: type_str = "Student" if self.type == UserType.STUDENT else "Teacher" return f"User({type_str}, {self.id})" + + @property + def name(self) -> str: + """str: User name""" + + if not self.__name: + self.__populate() + + return self.__name + + @property + def image(self) -> str: + """str: User image url""" + + if not self.__image: + self.__populate() + + return self.__image + + @property + def initials(self) -> str: + """str: User initials (only for teachers)""" + + if self.type == UserType.STUDENT: + return None + + if not self.__initials: + self.__populate() + + return self.__initials + + @property + def class_name(self) -> str: + """str: User class name (only for students)""" + + if self.type == UserType.TEACHER: + return None + + if not self.__class_name: + self.__populate() + + return self.__class_name From a8a2e0c12ed3d1f2d823beafc326bef81d121de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 22:14:05 +0200 Subject: [PATCH 10/29] vscode settings for sphinx thingy --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index bcb962c..c062001 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { "python.pythonPath": "C:\\Users\\dnorh\\AppData\\Local\\Programs\\Python\\Python39\\python.exe", - "restructuredtext.confPath": "${workspaceFolder}\\docs", "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.flake8Args": [ "--ignore=E501" - ] + ], + "esbonio.sphinx.confDir": "${workspaceFolder}\\docs" } \ No newline at end of file From 552a36baba42e05cbf486ca03a1b70b3d1ecc14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 22:15:51 +0200 Subject: [PATCH 11/29] Getting ready to write som docs soon --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0005627..7be8703 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'lectio.py' -copyright = '2020, dnorhoj' +copyright = '2022, dnorhoj' author = 'dnorhoj' # The full version, including alpha/beta/rc tags -release = '0.1.1' +release = '0.2.0' # -- General configuration --------------------------------------------------- From 0bdc6bf34e02a51e4554b7b663851d3b4a7bc4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 22:16:05 +0200 Subject: [PATCH 12/29] Removed old python path from settings.josn --- .vscode/settings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c062001..9b57497 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "python.pythonPath": "C:\\Users\\dnorh\\AppData\\Local\\Programs\\Python\\Python39\\python.exe", "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.flake8Args": [ From 26cbcb14e56a2a16d347541e02c45bb14f77c707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 23:09:39 +0200 Subject: [PATCH 13/29] New ways to get user objects from lectio.school.School object --- lectio/school.py | 75 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/lectio/school.py b/lectio/school.py index 1938b24..a4d8f96 100644 --- a/lectio/school.py +++ b/lectio/school.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING, List from bs4 import BeautifulSoup +import re -from .user import User +from .user import User, UserType if TYPE_CHECKING: from .lectio import Lectio @@ -32,7 +33,7 @@ def __populate(self) -> None: self.name = soup.find( "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "") - def get_user_by_letter(self, letter: str) -> List[User]: + def get_user_by_letter(self, letter: str, user_type: int = UserType.STUDENT) -> List[User]: """Get students by first letter of name Args: @@ -40,9 +41,17 @@ def get_user_by_letter(self, letter: str) -> List[User]: Returns: list(:class:`lectio.User`): List of students + + Raises: + NotImplementedError: If user_type is UserType.TEACHER, as this is not implemented """ - r = self._lectio._request("FindSkema.aspx?type=elev&forbogstav=" + letter) + if user_type == UserType.TEACHER: + raise NotImplementedError( + "Getting teachers by letter is not implemented") # TODO + + r = self._lectio._request( + "FindSkema.aspx?type=elev&forbogstav=" + letter.upper()) soup = BeautifulSoup(r.text, 'html.parser') @@ -51,11 +60,69 @@ def get_user_by_letter(self, letter: str) -> List[User]: # Container containing all students lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"}) + # Shouldn't happen in cases other than `len(letter) > 1` or if letter is not a valid character + if lst is None: + return [] + # Iterate and create user objects for i in lst.find_all("li"): - students.append(User(self._lectio, int(i.a["href"].split("=")[-1]))) + user_id = int(i.a["href"].split("=")[-1]) + + user_info = i.a.text.strip() + + # Search for name and class + search = re.search( + r"(?P.*) \((?P.*?) \d+?\)", user_info) + + if search is None: + continue + + students.append(User(self._lectio, + user_id, + lazy=True, + name=search.group("name"), + class_name=search.group("class_name"))) return students + def search_for_user(self, query: str, user_type: int = UserType.STUDENT) -> List[User]: + """Search for user + + Note: + This method is not very reliable, and will sometimes return no results. + Also, the query has to be from the beginning of the name. + + Example: Searching for "John" will return "John Doe", but searching for "Doe" will not. + + Args: + query (str): Name to search for + + Returns: + list(:class:`lectio.User`): List of users + """ + + res = [] + for student in self.get_user_by_letter(query[0], user_type): + if student.name.lower().startswith(query.lower()): + res.append(student) + + return res + + def get_all_users(self, user_type: int = UserType.STUDENT) -> List[User]: + """Get all users + + Args: + user_type (int): The type of the user (student or teacher) + + Returns: + list(:class:`lectio.User`): List of users + """ + + res = [] + for letter in "abcdefghijklmnopqrstuvwxyzæøå": + res.extend(self.get_user_by_letter(letter)) + + return res + def __repr__(self) -> str: return f"School({self.name})" From 191314df714313fdf7db9b462ba44747715a04c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Mon, 26 Sep 2022 23:10:59 +0200 Subject: [PATCH 14/29] Fixed problem where user image url was loaded from schedule page when you could generate it yourself --- lectio/user.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lectio/user.py b/lectio/user.py index 1996e65..4d82ba6 100644 --- a/lectio/user.py +++ b/lectio/user.py @@ -125,11 +125,6 @@ def __populate(self) -> None: elif self.type == UserType.TEACHER: self.__initials, self.__name, *_ = title.split(" - ") - src = soup.find( - "img", {"id": "s_m_HeaderContent_picctrlthumbimage"}).get("src") - - self.__image = f"https://www.lectio.dk{src}&fullsize=1" - def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']: """Get schedule for user @@ -165,7 +160,7 @@ def image(self) -> str: """str: User image url""" if not self.__image: - self.__populate() + self.__image = f"https://www.lectio.dk/lectio/{self._lectio.inst_id}/GetImage.aspx?pictureid={self.id}&fullsize=1" return self.__image From f646a798787ff860d22c62fb20e5bfaab975f192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Tue, 27 Sep 2022 12:28:13 +0200 Subject: [PATCH 15/29] Added github action to build and deploy to PyPi on release create --- .github/workflows/release.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..56ca745 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: Build and deploy to PyPi + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* \ No newline at end of file From 79decd4ca9b2b6d2ed7be3b7e04216bb1d941ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Tue, 27 Sep 2022 12:33:28 +0200 Subject: [PATCH 16/29] Should be ready for test --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56ca745..c02c114 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,8 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 - name: Install dependencies run: | python -m pip install --upgrade pip From f796832401b0b15ead874caed7cec63210c92ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 10:53:10 +0200 Subject: [PATCH 17/29] Small changes --- lectio/__init__.py | 5 +++ lectio/lectio.py | 22 +++++++---- lectio/school.py | 96 +++++++++++++++++++++++++++++++++++++--------- lectio/user.py | 17 +++++++- 4 files changed, 113 insertions(+), 27 deletions(-) diff --git a/lectio/__init__.py b/lectio/__init__.py index 254284c..c370c19 100644 --- a/lectio/__init__.py +++ b/lectio/__init__.py @@ -1,5 +1,10 @@ # flake8: noqa from .lectio import Lectio +from .user import User, UserType +from .school import School +from .helpers.schedule import Module +from .exceptions import * from . import user from . import lectio from . import exceptions +from . import school diff --git a/lectio/lectio.py b/lectio/lectio.py index d43c171..43df747 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -4,7 +4,7 @@ from . import exceptions -from .user import User, UserType +from .user import Me, User, UserType from .school import School @@ -21,7 +21,7 @@ class Lectio: https://www.lectio.dk/lectio/123/login.aspx - Here, the `123` would be my institution id. + Here, the ``123`` would be my institution id. """ def __init__(self, inst_id: int) -> None: @@ -59,6 +59,9 @@ def authenticate(self, username: str, password: str, save_creds: bool = True) -> print("Authenticated") except exceptions.IncorrectCredentialsError: print("Not authenticated") + exit(1) + + ... """ self.__CREDS = [] @@ -106,17 +109,20 @@ def _authenticate(self, username: str = None, password: str = None): raise exceptions.IncorrectCredentialsError( "Incorrect credentials provided!") - @property def school(self) -> School: - """:class:`lectio.school.School`: The school object for the authenticated user.""" + """Returns a :class:`School` object for the given institution id. + + Returns: + :class:`lectio.school.School`: The school object for the authenticated user. + """ return School(self) - def me(self) -> User: - """Gets own user object + def me(self) -> Me: + """Gets the authenticated user Returns: - :class:`lectio.profile.User`: User object + :class:`lectio.user.Me`: Own user object """ r = self._request("forside.aspx") @@ -136,7 +142,7 @@ def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool Args: user_id (str): The id of the user user_type (int): The type of the user (student or teacher) - check (bool): Whether to check if the user exists + check (bool): Whether to check if the user exists (slower) Returns: :class:`lectio.profile.User`: User object diff --git a/lectio/school.py b/lectio/school.py index a4d8f96..bef88e6 100644 --- a/lectio/school.py +++ b/lectio/school.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, List from bs4 import BeautifulSoup import re +from urllib.parse import quote from .user import User, UserType @@ -33,7 +34,71 @@ def __populate(self) -> None: self.name = soup.find( "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "") - def get_user_by_letter(self, letter: str, user_type: int = UserType.STUDENT) -> List[User]: + def get_teachers(self) -> List[User]: + """Get all teachers + + Returns: + list(:class:`lectio.User`): List of teachers + """ + + r = self._lectio._request("FindSkema.aspx?type=laerer&sortering=id") + + soup = BeautifulSoup(r.text, 'html.parser') + + teachers = [] + + # Container containing all teachers + lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"}) + + if lst is None: + return [] + + # Iterate and create user objects + for i in lst.find_all("li"): + user_id = int(i.a["href"].split("=")[-1]) + + user_name = i.a.contents[1].strip() + + initial_span = i.a.find('span') + + user_initials = None + if initial_span is not None: + user_initials = initial_span.text.strip() + + teachers.append(User(self._lectio, + user_id, + UserType.TEACHER, + lazy=True, + name=user_name, + initials=user_initials)) + + return teachers + + def search_for_teachers(self, query: str) -> List[User]: + """Search for teachers + + Note: + This method is not very reliable, and will sometimes return no results. + Also, the query has to be from the beginning of the name. + + Example: Searching for "John" will return "John Doe", but searching for "Doe" will not. + + Args: + query (str): Name to search for + + Returns: + list(:class:`lectio.User`): List of teachers + """ + + res = [] + + for teacher in self.get_teachers(): + if teacher.name.lower().startswith(query.lower()): + res.append(teacher) + + return res + + def get_students_by_letter(self, letter: str) -> List[User]: """Get students by first letter of name Args: @@ -41,17 +106,10 @@ def get_user_by_letter(self, letter: str, user_type: int = UserType.STUDENT) -> Returns: list(:class:`lectio.User`): List of students - - Raises: - NotImplementedError: If user_type is UserType.TEACHER, as this is not implemented """ - if user_type == UserType.TEACHER: - raise NotImplementedError( - "Getting teachers by letter is not implemented") # TODO - r = self._lectio._request( - "FindSkema.aspx?type=elev&forbogstav=" + letter.upper()) + "FindSkema.aspx?type=elev&forbogstav=" + quote(letter.upper())) soup = BeautifulSoup(r.text, 'html.parser') @@ -60,7 +118,7 @@ def get_user_by_letter(self, letter: str, user_type: int = UserType.STUDENT) -> # Container containing all students lst = soup.find("ul", {"class": "ls-columnlist mod-onechild"}) - # Shouldn't happen in cases other than `len(letter) > 1` or if letter is not a valid character + # Shouldn't happen in cases other than ``len(letter) > 1`` or if letter is not a valid character if lst is None: return [] @@ -85,7 +143,7 @@ def get_user_by_letter(self, letter: str, user_type: int = UserType.STUDENT) -> return students - def search_for_user(self, query: str, user_type: int = UserType.STUDENT) -> List[User]: + def search_for_students(self, query: str) -> List[User]: """Search for user Note: @@ -102,27 +160,29 @@ def search_for_user(self, query: str, user_type: int = UserType.STUDENT) -> List """ res = [] - for student in self.get_user_by_letter(query[0], user_type): + + for student in self.get_students_by_letter(query[0]): if student.name.lower().startswith(query.lower()): res.append(student) return res - def get_all_users(self, user_type: int = UserType.STUDENT) -> List[User]: - """Get all users - - Args: - user_type (int): The type of the user (student or teacher) + def get_all_students(self, user_type: int = UserType.STUDENT) -> List[User]: + """Get all students Returns: - list(:class:`lectio.User`): List of users + list(:class:`lectio.User`): List of students """ res = [] + for letter in "abcdefghijklmnopqrstuvwxyzæøå": res.extend(self.get_user_by_letter(letter)) return res + def search_for_users(self, query: str) -> List[User]: + return [*self.search_for_students(query), *self.search_for_teachers(query)] + def __repr__(self) -> str: return f"School({self.name})" diff --git a/lectio/user.py b/lectio/user.py index 4d82ba6..8b955b0 100644 --- a/lectio/user.py +++ b/lectio/user.py @@ -125,6 +125,11 @@ def __populate(self) -> None: elif self.type == UserType.TEACHER: self.__initials, self.__name, *_ = title.split(" - ") + src = soup.find( + "img", {"id": "s_m_HeaderContent_picctrlthumbimage"}).get("src") + + self.__image = f"https://www.lectio.dk{src}&fullsize=1" + def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']: """Get schedule for user @@ -160,7 +165,7 @@ def image(self) -> str: """str: User image url""" if not self.__image: - self.__image = f"https://www.lectio.dk/lectio/{self._lectio.inst_id}/GetImage.aspx?pictureid={self.id}&fullsize=1" + self.__populate() return self.__image @@ -187,3 +192,13 @@ def class_name(self) -> str: self.__populate() return self.__class_name + + def __eq__(self, __o: object) -> bool: + if not isinstance(__o, User): + return False + + return self.id == __o.id and self.type == __o.type + + +class Me(User): + pass From a507e9834010f18d7fb299b2914fabc1f9a69786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 10:54:58 +0200 Subject: [PATCH 18/29] I forgor to change something --- lectio/school.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lectio/school.py b/lectio/school.py index bef88e6..6bb9e3a 100644 --- a/lectio/school.py +++ b/lectio/school.py @@ -167,7 +167,7 @@ def search_for_students(self, query: str) -> List[User]: return res - def get_all_students(self, user_type: int = UserType.STUDENT) -> List[User]: + def get_all_students(self) -> List[User]: """Get all students Returns: @@ -177,7 +177,7 @@ def get_all_students(self, user_type: int = UserType.STUDENT) -> List[User]: res = [] for letter in "abcdefghijklmnopqrstuvwxyzæøå": - res.extend(self.get_user_by_letter(letter)) + res.extend(self.get_students_by_letter(letter)) return res From 1961a02867848441c0fcddbf3293d99537a924d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 12:23:41 +0200 Subject: [PATCH 19/29] Restructuring --- lectio/__init__.py | 8 +++---- lectio/lectio.py | 34 ++------------------------- lectio/{ => models}/school.py | 44 +++++++++++++++++++++++++++++++++-- lectio/{ => models}/user.py | 9 +++---- 4 files changed, 53 insertions(+), 42 deletions(-) rename lectio/{ => models}/school.py (79%) rename lectio/{ => models}/user.py (95%) diff --git a/lectio/__init__.py b/lectio/__init__.py index c370c19..6e2e728 100644 --- a/lectio/__init__.py +++ b/lectio/__init__.py @@ -1,10 +1,10 @@ # flake8: noqa from .lectio import Lectio -from .user import User, UserType -from .school import School +from .models.user import User, UserType +from .models.school import School from .helpers.schedule import Module from .exceptions import * -from . import user +from .models import user from . import lectio from . import exceptions -from . import school +from .models import school diff --git a/lectio/lectio.py b/lectio/lectio.py index 43df747..5fd9955 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -4,8 +4,8 @@ from . import exceptions -from .user import Me, User, UserType -from .school import School +from .models.user import Me, User, UserType +from .models.school import School class Lectio: @@ -136,36 +136,6 @@ def me(self) -> Me: return self.get_user(user_id, check=False) - def get_user(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> User: - """Gets a user by their id - - Args: - user_id (str): The id of the user - user_type (int): The type of the user (student or teacher) - check (bool): Whether to check if the user exists (slower) - - Returns: - :class:`lectio.profile.User`: User object - - Raises: - :class:`lectio.exceptions.UserDoesNotExistError`: When the user does not exist - """ - - if check: - type_str = "elev" if user_type == UserType.STUDENT else "laerer" - - # Check if user exists - r = self._request( - f"SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") - - soup = BeautifulSoup(r.text, 'html.parser') - - if soup.title.string.strip().startswith("Fejl - Lectio"): - raise exceptions.UserDoesNotExistError( - f"The user with the id '{user_id}' does not exist!") - - return User(self, user_id, user_type) - def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: r = self.__session.request( method, f"https://www.lectio.dk/lectio/{str(self.inst_id)}/{url}", **kwargs) diff --git a/lectio/school.py b/lectio/models/school.py similarity index 79% rename from lectio/school.py rename to lectio/models/school.py index 6bb9e3a..2e10357 100644 --- a/lectio/school.py +++ b/lectio/models/school.py @@ -4,9 +4,10 @@ from urllib.parse import quote from .user import User, UserType +from ..import exceptions if TYPE_CHECKING: - from .lectio import Lectio + from ..lectio import Lectio class School: @@ -34,6 +35,36 @@ def __populate(self) -> None: self.name = soup.find( "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "") + def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> User: + """Gets a user by their id + + Args: + user_id (str): The id of the user + user_type (int): The type of the user (student or teacher) + check (bool): Whether to check if the user exists (slower) + + Returns: + :class:`lectio.user.User`: User object + + Raises: + :class:`lectio.exceptions.UserDoesNotExistError`: When the user does not exist + """ + + if check: + type_str = "elev" if user_type == UserType.STUDENT else "laerer" + + # Check if user exists + r = self._request( + f"SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") + + soup = BeautifulSoup(r.text, 'html.parser') + + if soup.title.string.strip().startswith("Fejl - Lectio"): + raise exceptions.UserDoesNotExistError( + f"The {UserType.get_str(user_type, True)} with the id '{user_id}' does not exist!") + + return User(self, user_id, user_type) + def get_teachers(self) -> List[User]: """Get all teachers @@ -171,7 +202,7 @@ def get_all_students(self) -> List[User]: """Get all students Returns: - list(:class:`lectio.User`): List of students + list(:class:`User`): List of students """ res = [] @@ -182,6 +213,15 @@ def get_all_students(self) -> List[User]: return res def search_for_users(self, query: str) -> List[User]: + """Search for user + + Args: + query (str): Name to search for + + Returns: + list(:class:`lectio.user.User`): List of users + """ + return [*self.search_for_students(query), *self.search_for_teachers(query)] def __repr__(self) -> str: diff --git a/lectio/user.py b/lectio/models/user.py similarity index 95% rename from lectio/user.py rename to lectio/models/user.py index 8b955b0..d29461a 100644 --- a/lectio/user.py +++ b/lectio/models/user.py @@ -1,12 +1,12 @@ from bs4 import BeautifulSoup from typing import TYPE_CHECKING, List -from .helpers.schedule import get_schedule +from ..helpers.schedule import get_schedule if TYPE_CHECKING: from datetime import datetime - from .helpers.schedule import Module - from .lectio import Lectio + from ..helpers.schedule import Module + from ..lectio import Lectio class UserType: @@ -62,7 +62,8 @@ class User: Note: This class should not be instantiated directly, - but rather through the :meth:`lectio.Lectio.me` or :meth:`lectio.Lectio.get_user` methods. + but rather through the :meth:`lectio.Lectio.get_user` + or :meth:`lectio.school.School.search_for_users` methods or similar. Args: lectio (:class:`lectio.Lectio`): Lectio object From 63645dc15ac43e10b4e409bad680191770fb50a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 12:32:40 +0200 Subject: [PATCH 20/29] Bugfixes; after remodeling --- lectio/__init__.py | 9 +++------ lectio/lectio.py | 4 ++-- lectio/models/__init__.py | 3 +++ lectio/models/school.py | 12 ++++++------ lectio/models/user.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 lectio/models/__init__.py diff --git a/lectio/__init__.py b/lectio/__init__.py index 6e2e728..934296d 100644 --- a/lectio/__init__.py +++ b/lectio/__init__.py @@ -1,10 +1,7 @@ # flake8: noqa from .lectio import Lectio -from .models.user import User, UserType -from .models.school import School from .helpers.schedule import Module from .exceptions import * -from .models import user -from . import lectio -from . import exceptions -from .models import school +from .models.user import User, UserType +from .models.school import School +from .models import * \ No newline at end of file diff --git a/lectio/lectio.py b/lectio/lectio.py index 5fd9955..83d23bb 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -122,7 +122,7 @@ def me(self) -> Me: """Gets the authenticated user Returns: - :class:`lectio.user.Me`: Own user object + :class:`lectio.models.user.Me`: Own user object """ r = self._request("forside.aspx") @@ -134,7 +134,7 @@ def me(self) -> Me: user_id = re.match(r'.*id=([0-9]+)$', content)[1] - return self.get_user(user_id, check=False) + return Me(self, user_id, UserType.STUDENT) # TODO; Add support for teachers def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: r = self.__session.request( diff --git a/lectio/models/__init__.py b/lectio/models/__init__.py new file mode 100644 index 0000000..2c2b9de --- /dev/null +++ b/lectio/models/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .school import School +from .user import User, Me, UserType diff --git a/lectio/models/school.py b/lectio/models/school.py index 2e10357..bbf8bac 100644 --- a/lectio/models/school.py +++ b/lectio/models/school.py @@ -7,7 +7,7 @@ from ..import exceptions if TYPE_CHECKING: - from ..lectio import Lectio + from .. import Lectio class School: @@ -44,7 +44,7 @@ def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: check (bool): Whether to check if the user exists (slower) Returns: - :class:`lectio.user.User`: User object + :class:`lectio.models.user.User`: User object Raises: :class:`lectio.exceptions.UserDoesNotExistError`: When the user does not exist @@ -69,7 +69,7 @@ def get_teachers(self) -> List[User]: """Get all teachers Returns: - list(:class:`lectio.User`): List of teachers + list(:class:`lectio.models.user.User`): List of teachers """ r = self._lectio._request("FindSkema.aspx?type=laerer&sortering=id") @@ -118,7 +118,7 @@ def search_for_teachers(self, query: str) -> List[User]: query (str): Name to search for Returns: - list(:class:`lectio.User`): List of teachers + list(:class:`lectio.models.user.User`): List of teachers """ res = [] @@ -136,7 +136,7 @@ def get_students_by_letter(self, letter: str) -> List[User]: letter (str): Letter to search for Returns: - list(:class:`lectio.User`): List of students + list(:class:`lectio.models.user.User`): List of students """ r = self._lectio._request( @@ -219,7 +219,7 @@ def search_for_users(self, query: str) -> List[User]: query (str): Name to search for Returns: - list(:class:`lectio.user.User`): List of users + list(:class:`lectio.models.user.User`): List of users """ return [*self.search_for_students(query), *self.search_for_teachers(query)] diff --git a/lectio/models/user.py b/lectio/models/user.py index d29461a..fb47639 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -18,7 +18,7 @@ class UserType: Example: >>> from lectio import Lectio - >>> from lectio.user import UserType + >>> from lectio.models.user import UserType >>> lec = Lectio(123) >>> lec.authenticate("username", "password") >>> me = lec.me() From eeb971d3af408cccdd7b3faa4b85de6226b57fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 12:35:46 +0200 Subject: [PATCH 21/29] Added caching to lec.me and lec.school --- lectio/lectio.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lectio/lectio.py b/lectio/lectio.py index 83d23bb..799abf2 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -24,6 +24,9 @@ class Lectio: Here, the ``123`` would be my institution id. """ + __school: School = None + __me: Me = None + def __init__(self, inst_id: int) -> None: self.__CREDS = [] self.__session = requests.Session() @@ -116,7 +119,10 @@ def school(self) -> School: :class:`lectio.school.School`: The school object for the authenticated user. """ - return School(self) + if self.__school is None: + self.__school = School(self) + + return self.__school def me(self) -> Me: """Gets the authenticated user @@ -125,16 +131,20 @@ def me(self) -> Me: :class:`lectio.models.user.Me`: Own user object """ - r = self._request("forside.aspx") + if self.__me is None: + r = self._request("forside.aspx") + + soup = BeautifulSoup(r.text, 'html.parser') - soup = BeautifulSoup(r.text, 'html.parser') + content = soup.find( + 'meta', {'name': 'msapplication-starturl'}).attrs.get('content') - content = soup.find( - 'meta', {'name': 'msapplication-starturl'}).attrs.get('content') + user_id = re.match(r'.*id=([0-9]+)$', content)[1] - user_id = re.match(r'.*id=([0-9]+)$', content)[1] + # TODO; Add support for teachers + self.__me = Me(self, user_id, UserType.STUDENT) - return Me(self, user_id, UserType.STUDENT) # TODO; Add support for teachers + return self.__me def _request(self, url: str, method: str = "GET", **kwargs) -> requests.Response: r = self.__session.request( From 62409eb40064eed0b71f41ecbf8abf0b316fd915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 12:39:33 +0200 Subject: [PATCH 22/29] Fixed bug --- lectio/models/school.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lectio/models/school.py b/lectio/models/school.py index bbf8bac..1f2d6c3 100644 --- a/lectio/models/school.py +++ b/lectio/models/school.py @@ -54,7 +54,7 @@ def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: type_str = "elev" if user_type == UserType.STUDENT else "laerer" # Check if user exists - r = self._request( + r = self._lectio._request( f"SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") soup = BeautifulSoup(r.text, 'html.parser') From 17d338f15242e9151309d534d8aa6568a55db2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 12:58:06 +0200 Subject: [PATCH 23/29] Changed UserType to be enum instead of int --- lectio/helpers/__init__.py | 1 + lectio/models/school.py | 8 +++----- lectio/models/user.py | 41 ++++++++++++++++---------------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/lectio/helpers/__init__.py b/lectio/helpers/__init__.py index b7ee570..aa6e1c6 100644 --- a/lectio/helpers/__init__.py +++ b/lectio/helpers/__init__.py @@ -1 +1,2 @@ +# flake8: noqa from .schedule import * diff --git a/lectio/models/school.py b/lectio/models/school.py index 1f2d6c3..81ba768 100644 --- a/lectio/models/school.py +++ b/lectio/models/school.py @@ -35,7 +35,7 @@ def __populate(self) -> None: self.name = soup.find( "div", {"id": "s_m_masterleftDiv"}).text.strip().split("\n")[0].replace("\r", "") - def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: bool = True) -> User: + def get_user_by_id(self, user_id: str, user_type: UserType = UserType.STUDENT, check: bool = True) -> User: """Gets a user by their id Args: @@ -51,11 +51,9 @@ def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: """ if check: - type_str = "elev" if user_type == UserType.STUDENT else "laerer" - # Check if user exists r = self._lectio._request( - f"SkemaNy.aspx?type={type_str}&{type_str}id={user_id}") + f"SkemaNy.aspx?type={user_type}&{user_type}id={user_id}") soup = BeautifulSoup(r.text, 'html.parser') @@ -63,7 +61,7 @@ def get_user_by_id(self, user_id: str, user_type: int = UserType.STUDENT, check: raise exceptions.UserDoesNotExistError( f"The {UserType.get_str(user_type, True)} with the id '{user_id}' does not exist!") - return User(self, user_id, user_type) + return User(self._lectio, user_id, user_type) def get_teachers(self) -> List[User]: """Get all teachers diff --git a/lectio/models/user.py b/lectio/models/user.py index fb47639..7362174 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -1,3 +1,4 @@ +from enum import Enum from bs4 import BeautifulSoup from typing import TYPE_CHECKING, List @@ -9,7 +10,7 @@ from ..lectio import Lectio -class UserType: +class UserType(Enum): """User types Attributes: @@ -31,28 +32,23 @@ class UserType: STUDENT = 0 TEACHER = 1 - @staticmethod - def get_str(user_type: int, en: bool = False) -> str: - """Get string representation of user type for lectio interface - - Args: - type (int): User type - en (bool): Whether to return english string instead of Danish + def get_str(self) -> str: + """Get string representation of user type for lectio interface in english Returns: str: String representation of user type """ - if user_type == UserType.STUDENT: - if en: - return "student" - else: - return "elev" - elif user_type == UserType.TEACHER: - if en: - return "teacher" - else: - return "laerer" + if self.value == self.STUDENT.value: + return "student" + elif self.value == self.TEACHER.value: + return "teacher" + + def __str__(self) -> str: + if self.value == self.STUDENT.value: + return "elev" + elif self.value == self.TEACHER.value: + return "laerer" class User: @@ -85,13 +81,10 @@ class User: __class_name = None __image = None - def __init__(self, lectio: 'Lectio', user_id: int, user_type: int = UserType.STUDENT, *, lazy=False, **user_data) -> None: + def __init__(self, lectio: 'Lectio', user_id: int, user_type: UserType = UserType.STUDENT, *, lazy=False, **user_data) -> None: self._lectio = lectio self.id = user_id - if user_type not in [UserType.STUDENT, UserType.TEACHER]: - raise ValueError("Invalid user type") - self.type = user_type if not lazy: @@ -112,7 +105,7 @@ def __populate(self) -> None: # Get user's schedule for today r = self._lectio._request( - f"SkemaNy.aspx?type={UserType.get_str(self.type)}&{UserType.get_str(self.type)}id={self.id}") + f"SkemaNy.aspx?type={self.type}&{self.type}id={self.id}") soup = BeautifulSoup(r.text, "html.parser") @@ -142,7 +135,7 @@ def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: return get_schedule( self._lectio, - [f"{UserType.get_str(self.type, True)}sel={self.id}"], + [f"{self.type.get_str()}sel={self.id}"], start_date, end_date, strip_time From 237e580b99ccbc6d3fa5aae179bafc83fbbe4b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 13:28:50 +0200 Subject: [PATCH 24/29] Should be pretty stable now --- .vscode/settings.json | 2 +- Pipfile.lock | 286 ++++++++++++++++++---------------------- lectio/models/school.py | 2 +- lectio/models/user.py | 26 ++-- 4 files changed, 141 insertions(+), 175 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b57497..e99e217 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "python.linting.flake8Args": [ "--ignore=E501" ], - "esbonio.sphinx.confDir": "${workspaceFolder}\\docs" + "esbonio.sphinx.confDir": "${workspaceFolder}/docs" } \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock index 5baab51..08b42a6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,58 +16,59 @@ "default": { "beautifulsoup4": { "hashes": [ - "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", - "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891" + "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30", + "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693" ], "index": "pypi", - "version": "==4.10.0" + "version": "==4.11.1" }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "version": "==2021.10.8" + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" }, "charset-normalizer": { "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3'", - "version": "==2.0.11" + "markers": "python_version >= '3.6'", + "version": "==2.1.1" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.1" }, "soupsieve": { "hashes": [ - "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb", - "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9" + "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759", + "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d" ], "markers": "python_version >= '3.6'", - "version": "==2.3.1" + "version": "==2.3.2.post1" }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" } }, "develop": { @@ -80,34 +81,35 @@ }, "autopep8": { "hashes": [ - "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979", - "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f" + "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087", + "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142" ], "index": "pypi", - "version": "==1.6.0" + "version": "==1.7.0" }, "babel": { "hashes": [ - "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", - "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" + "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51", + "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.1" + "markers": "python_version >= '3.6'", + "version": "==2.10.3" }, "certifi": { "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14", + "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382" ], - "version": "==2021.10.8" + "markers": "python_version >= '3.6'", + "version": "==2022.9.24" }, "charset-normalizer": { "hashes": [ - "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45", - "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3'", - "version": "==2.0.11" + "markers": "python_version >= '3.6'", + "version": "==2.1.1" }, "docutils": { "hashes": [ @@ -119,117 +121,89 @@ }, "flake8": { "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" + "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", + "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" ], "index": "pypi", - "version": "==4.0.1" + "version": "==5.0.4" }, "idna": { "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" ], - "markers": "python_version >= '3'", - "version": "==3.3" + "markers": "python_version >= '3.5'", + "version": "==3.4" }, "imagesize": { "hashes": [ - "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", - "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" + "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", + "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.0" + "version": "==1.4.1" }, "jinja2": { "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" + "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", + "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" + "markers": "python_version >= '3.7'", + "version": "==3.1.2" }, "markupsafe": { "hashes": [ - "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", - "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", - "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", - "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", - "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", - "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", - "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", - "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", - "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", - "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", - "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", - "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", - "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", - "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", - "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", - "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", - "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", - "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", - "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", - "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", - "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", - "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", - "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", - "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", - "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", - "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", - "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", - "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", - "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", - "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", - "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", - "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", - "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", - "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", - "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", - "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", - "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", - "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", - "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", - "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", - "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", - "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", - "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", - "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", - "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", - "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", - "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", - "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", - "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", - "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", - "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", - "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", - "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", - "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", - "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", - "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", - "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", - "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", - "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", - "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", - "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", - "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", - "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", - "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", - "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", - "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", - "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", - "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", - "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", + "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", + "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", + "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", + "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", + "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", + "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", + "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", + "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", + "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", + "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", + "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", + "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", + "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", + "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", + "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", + "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", + "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", + "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", + "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", + "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", + "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", + "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", + "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", + "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", + "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", + "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", + "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", + "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", + "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", + "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", + "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", + "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", + "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", + "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", + "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", + "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", + "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", + "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", + "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.1" }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "packaging": { "hashes": [ @@ -241,50 +215,50 @@ }, "pycodestyle": { "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", + "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" + "markers": "python_version >= '3.6'", + "version": "==2.9.1" }, "pyflakes": { "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" + "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", + "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" + "markers": "python_version >= '3.6'", + "version": "==2.5.0" }, "pygments": { "hashes": [ - "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65", - "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a" + "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", + "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" ], - "markers": "python_version >= '3.5'", - "version": "==2.11.2" + "markers": "python_version >= '3.6'", + "version": "==2.13.0" }, "pyparsing": { "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", + "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" + "markers": "python_full_version >= '3.6.8'", + "version": "==3.0.9" }, "pytz": { "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" + "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", + "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" ], - "version": "==2021.3" + "version": "==2022.2.1" }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.1" }, "snowballstemmer": { "hashes": [ @@ -295,11 +269,11 @@ }, "sphinx": { "hashes": [ - "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe", - "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc" + "sha256:7225c104dc06169eb73b061582c4bc84a9594042acae6c1582564de274b7df2f", + "sha256:9150a8ed2e98d70e778624373f183c5498bf429dd605cf7b63e80e2a166c35a5" ], "index": "pypi", - "version": "==4.4.0" + "version": "==5.2.2" }, "sphinx-rtd-theme": { "hashes": [ @@ -367,11 +341,11 @@ }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e", + "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'", + "version": "==1.26.12" } } } diff --git a/lectio/models/school.py b/lectio/models/school.py index 81ba768..5f7b327 100644 --- a/lectio/models/school.py +++ b/lectio/models/school.py @@ -40,7 +40,7 @@ def get_user_by_id(self, user_id: str, user_type: UserType = UserType.STUDENT, c Args: user_id (str): The id of the user - user_type (int): The type of the user (student or teacher) + user_type (:class:`lectio.models.user.UserType`): The type of the user (student or teacher) check (bool): Whether to check if the user exists (slower) Returns: diff --git a/lectio/models/user.py b/lectio/models/user.py index 7362174..4bf7d7c 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -11,11 +11,7 @@ class UserType(Enum): - """User types - - Attributes: - STUDENT (int): Student - TEACHER (int): Teacher + """User types enum Example: >>> from lectio import Lectio @@ -64,16 +60,12 @@ class User: Args: lectio (:class:`lectio.Lectio`): Lectio object user_id (int): User id - user_type (int): User type (UserType.STUDENT or UserType.TEACHER) + user_type (:class:`lectio.models.user.UserType`): User type (UserType.STUDENT or UserType.TEACHER) lazy (bool): Whether to not populate user object on instantiation (default: False) Attributes: id (int): User id - type (int): User type (UserType.STUDENT or UserType.TEACHER) - name (str): Full name of user - initials (str|None): Initials of user if user is a teacher - class_name (str|None): Class of user if user is a student - image (str): User image url + type (:class:`lectio.models.user.UserType`): User type (UserType.STUDENT or UserType.TEACHER) """ __name = None @@ -142,12 +134,11 @@ def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: ) def __repr__(self) -> str: - type_str = "Student" if self.type == UserType.STUDENT else "Teacher" - return f"User({type_str}, {self.id})" + return f"User({self.type.get_str().capitalize()}, {self.id})" @property def name(self) -> str: - """str: User name""" + """str: User's name""" if not self.__name: self.__populate() @@ -156,7 +147,7 @@ def name(self) -> str: @property def image(self) -> str: - """str: User image url""" + """str: User's image url""" if not self.__image: self.__populate() @@ -165,7 +156,7 @@ def image(self) -> str: @property def initials(self) -> str: - """str: User initials (only for teachers)""" + """str|None: User's initials (only for teachers)""" if self.type == UserType.STUDENT: return None @@ -177,7 +168,7 @@ def initials(self) -> str: @property def class_name(self) -> str: - """str: User class name (only for students)""" + """str|None: User's class name (only for students)""" if self.type == UserType.TEACHER: return None @@ -195,4 +186,5 @@ def __eq__(self, __o: object) -> bool: class Me(User): + # TODO: Add methods for getting grades, absences, etc. pass From 1f167ebb1421b79afe669a6f413da828a57b16da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 13:31:13 +0200 Subject: [PATCH 25/29] Update to documentation --- docs/index.rst | 25 ++++++++++++++++++++-- docs/installation.rst | 18 +++++++++++++++- docs/quickstart.rst | 29 ++++++++++++++++++++++++- docs/reference.rst | 49 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 7743660..6e60ec0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,30 @@ -Welcome to lectio.py's documentation! +Welcome to lectio.py ======================================= +Lectio.py is a Python library for reading and interacting with lectio.dk, +a Danish school management system. + +.. note:: + This library is not affiliated with lectio.dk or macom in any way. + Nor is it endorsed by them. It scrapes the website as there is no + official API. + +**Features:** + +- Pythonic API +- Easy to use + +Information +----------- + +**Useful links:** :doc:`/installation` | :doc:`/quickstart` +**Reference:** :doc:`/reference` | :doc:`/exceptions` + +Table of contents +----------------- + .. toctree:: :maxdepth: 2 - :caption: Contents: installation quickstart diff --git a/docs/installation.rst b/docs/installation.rst index 75a91e8..ca24c90 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,4 +1,20 @@ Installation ============ -TODO \ No newline at end of file +To install lectio.py and its dependencies, you can simply install it from PyPi:: + + pip install lectio.py + +or clone the repository and install it manually:: + + git clone https://github.com/dnorhoj/Lectio.py.git + cd lectio.py + python setup.py install + +You can check if the installation was successful by running:: + + python + + >>> import lectio + >>> lectio.__version__ + diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 474a449..8925be2 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,4 +1,31 @@ Quickstart ========== -TODO \ No newline at end of file +Let's start with a simple example, where we will get your schedule for today. + +Schedule +-------- + +Create a new file, and add the following code: + +.. code-block:: python + + from lectio import Lectio + from datetime import datetime, timedelta + + lec = Lectio('', '') + + # Get my user object + me = lec.me() + + # Get the schedule for today + schedule = me.get_schedule(datetime.now(), datetime.now() + timedelta(days=1)) + +We now have a list of all :class:`lectio.helpers.Module` in the ``schedule`` variable. Let's print it: + +.. code-block:: python + + for module in schedule: + print(module.title, module.teacher, module.room) + +**More to come...!** diff --git a/docs/reference.rst b/docs/reference.rst index 0683dfb..6e11b68 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1,6 +1,53 @@ +.. currentmodule:: lectio + API Reference ============= -.. automodule:: lectio.lectio +Lectio +------ + +.. autoclass:: Lectio + :members: + :undoc-members: + + +School +------ + +.. autoclass:: lectio.school.School + :members: + :undoc-members: + +User +---- + +User +^^^^ + +.. autoclass:: lectio.models.user.User + :members: + :undoc-members: + +Me +^^^ + +.. autoclass:: lectio.models.user.Me + :members: + :undoc-members: + +User types +^^^^^^^^^^ + +.. autoclass:: lectio.models.user.UserType + :members: + :undoc-members: + +Misc +---- + +Module +^^^^^^ + +.. autoclass:: lectio.helpers.schedule.Module :members: :undoc-members: \ No newline at end of file From 6fbf4fbb5c4a24f7a79c3f6d43467be4d59d21ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 15:03:13 +0200 Subject: [PATCH 26/29] Changes in documentation --- docs/reference.rst | 2 +- lectio/lectio.py | 4 ++-- lectio/models/user.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6e11b68..22c82e4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -14,7 +14,7 @@ Lectio School ------ -.. autoclass:: lectio.school.School +.. autoclass:: lectio.models.school.School :members: :undoc-members: diff --git a/lectio/lectio.py b/lectio/lectio.py index 799abf2..14f4da0 100644 --- a/lectio/lectio.py +++ b/lectio/lectio.py @@ -113,10 +113,10 @@ def _authenticate(self, username: str = None, password: str = None): "Incorrect credentials provided!") def school(self) -> School: - """Returns a :class:`School` object for the given institution id. + """Returns a :class:`lectio.models.school.School` object for the given institution id. Returns: - :class:`lectio.school.School`: The school object for the authenticated user. + :class:`lectio.models.school.School`: The school object for the authenticated user. """ if self.__school is None: diff --git a/lectio/models/user.py b/lectio/models/user.py index 4bf7d7c..f410af3 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -55,7 +55,7 @@ class User: Note: This class should not be instantiated directly, but rather through the :meth:`lectio.Lectio.get_user` - or :meth:`lectio.school.School.search_for_users` methods or similar. + or :meth:`lectio.models.school.School.search_for_users` methods or similar. Args: lectio (:class:`lectio.Lectio`): Lectio object From d76d8d21066595be7ee4260a4b443dd6416066e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 15:10:42 +0200 Subject: [PATCH 27/29] Fixed bug where get schedule would crash if no modules, and explained strip_time better --- lectio/helpers/schedule.py | 9 +++++++-- lectio/models/user.py | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py index 7d2d30b..74ea390 100644 --- a/lectio/helpers/schedule.py +++ b/lectio/helpers/schedule.py @@ -95,8 +95,13 @@ def get_schedule(lectio: 'Lectio', params: List[str], start_date: datetime, end_ soup = BeautifulSoup(schedule_request.text, 'html.parser') - modules = soup.find( - "table", class_="list texttop lf-grid").findChildren('tr', class_=None) + module_table = soup.find( + "table", class_="list texttop lf-grid") + + if not module_table: + return [] + + modules = module_table.findChildren('tr', class_=None) schedule = [] for module in modules: diff --git a/lectio/models/user.py b/lectio/models/user.py index f410af3..275b37d 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -122,7 +122,8 @@ def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: Args: start_date (:class:`datetime.datetime`): Start date end_date (:class:`datetime.datetime`): End date - strip_time (bool): Strip time from datetime objects (default: True) + strip_time (bool): Whether to remove hours, minutes and seconds from date info, also adds 1 day to end time. + Basically just allows you to put in a random time of two days, and still get all modules from all the days including start and end date. """ return get_schedule( From e5874bf2504760afefb4ae5b5b4db84f0be83e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 15:20:59 +0200 Subject: [PATCH 28/29] Fixed bug where schedule finder will crash if you select a range with no modules --- lectio/helpers/schedule.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lectio/helpers/schedule.py b/lectio/helpers/schedule.py index 74ea390..6e14174 100644 --- a/lectio/helpers/schedule.py +++ b/lectio/helpers/schedule.py @@ -101,6 +101,10 @@ def get_schedule(lectio: 'Lectio', params: List[str], start_date: datetime, end_ if not module_table: return [] + # Not a good way of checking if there are no modules, but it works + if module_table.find("div", {"class": "noRecord"}): + return [] + modules = module_table.findChildren('tr', class_=None) schedule = [] From 91c2a691f045e39292dc2bb189063ecfea20226c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Norh=C3=B8j?= Date: Wed, 28 Sep 2022 15:41:12 +0200 Subject: [PATCH 29/29] Added note to get_schedule --- lectio/models/user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lectio/models/user.py b/lectio/models/user.py index 275b37d..bd3f78b 100644 --- a/lectio/models/user.py +++ b/lectio/models/user.py @@ -119,6 +119,11 @@ def __populate(self) -> None: def get_schedule(self, start_date: 'datetime', end_date: 'datetime', strip_time: bool = True) -> List['Module']: """Get schedule for user + Note: + As lectio is weird, you can only get a schedule for a range + that is less than one month. + If you specify a range greater than one month, you will get an empty return list. + Args: start_date (:class:`datetime.datetime`): Start date end_date (:class:`datetime.datetime`): End date