mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-07-01 19:44:57 +02:00
Updated files. getting ready for publishing. Some issues
This commit is contained in:
0
temmies/__init__.py
Normal file
0
temmies/__init__.py
Normal file
71
temmies/course.py
Normal file
71
temmies/course.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""
|
||||
Houses the Course class which is used to represent a course in a year.
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from requests import Session
|
||||
from .exercise_group import ExerciseGroup
|
||||
from .exceptions.course_unavailable import CourseUnavailable
|
||||
from .exceptions.illegal_action import IllegalAction
|
||||
|
||||
|
||||
class Course:
|
||||
"""
|
||||
get_groups: Get all groups in a course. Set full to True to get all subgroups.
|
||||
get_group: Get a group by name. Set full to True to get all subgroups.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, name: str, session: Session, parent):
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.__session = session
|
||||
self.__parent = parent
|
||||
self.__request = self.__session.get(self.url)
|
||||
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
||||
|
||||
self.__course_available(self.__session.get(self.url))
|
||||
|
||||
def __str__(self):
|
||||
return f"Course {self.name} in year {self.__parent.year}"
|
||||
|
||||
def __course_available(self, r):
|
||||
# Check if we got an error
|
||||
# print(self.url)
|
||||
if "Something went wrong" in r.text:
|
||||
raise CourseUnavailable(
|
||||
message="'Something went wrong'. Course most likely not found. "
|
||||
)
|
||||
|
||||
def get_groups(self, full: bool = False) -> list[ExerciseGroup]:
|
||||
"""
|
||||
get_groups(full: bool = False) -> list[ExerciseGroup]
|
||||
Get all groups in a course. Set full to True to get all subgroups.
|
||||
"""
|
||||
section = self.__raw.find("div", class_="ass-children")
|
||||
entries = section.find_all("a", href=True)
|
||||
return [
|
||||
ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{x['href']}",
|
||||
x,
|
||||
self.__session,
|
||||
full
|
||||
)
|
||||
for x in entries
|
||||
]
|
||||
|
||||
# BAD: Repeated code!!!!
|
||||
def get_group(self, name: str, full: bool = False) -> ExerciseGroup:
|
||||
"""
|
||||
get_group(name:str, full:bool = False) -> ExerciseGroup
|
||||
Get a single group by name. Set full to True to get all subgroups as well.
|
||||
"""
|
||||
group = self.__raw.find("a", text=name)
|
||||
if not group:
|
||||
raise IllegalAction(message=f"No such group found: {name}")
|
||||
|
||||
return ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{group['href']}",
|
||||
group,
|
||||
self.__session,
|
||||
full
|
||||
)
|
6
temmies/exceptions/course_unavailable.py
Normal file
6
temmies/exceptions/course_unavailable.py
Normal file
@ -0,0 +1,6 @@
|
||||
""" This module contains the CourseUnavailable exception. """
|
||||
|
||||
class CourseUnavailable(Exception):
|
||||
"""CourseUnavailable Exception"""
|
||||
def __init__(self, message: str = ""):
|
||||
super().__init__(f"Course unavailable: {message}")
|
8
temmies/exceptions/illegal_action.py
Normal file
8
temmies/exceptions/illegal_action.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
Illegal Action Exception
|
||||
"""
|
||||
|
||||
class IllegalAction(Exception):
|
||||
"""Illegal Action Exception"""
|
||||
def __init__(self, message: str = ""):
|
||||
super().__init__(f"Illegal action: {message}")
|
347
temmies/exercise_group.py
Normal file
347
temmies/exercise_group.py
Normal file
@ -0,0 +1,347 @@
|
||||
"""
|
||||
Houses the ExerciseGroup class.
|
||||
Represents a group of exercises or a single exercise.
|
||||
|
||||
"""
|
||||
|
||||
from json import loads
|
||||
from time import sleep
|
||||
from bs4 import BeautifulSoup
|
||||
from .exceptions.illegal_action import IllegalAction
|
||||
from .submission import Submission
|
||||
|
||||
class ExerciseGroup:
|
||||
"""
|
||||
Methods:
|
||||
`submit`: submit to an exercise
|
||||
`get_group`: get a group by name
|
||||
`download_tcs`: download test cases
|
||||
`download_files`: download files
|
||||
|
||||
`find_status`: get status for an exercise by name
|
||||
`get_all_statuses`: get all available statuses(useful for multiple exercises)
|
||||
`get_status(idx=0)`: get the available statuses for the exercise. Set the idx if you want to get a specific submission.
|
||||
Attributes:
|
||||
|
||||
`am_exercise`: returns bool which tells you if the instance is an exercise
|
||||
`folders`: folders in the folder
|
||||
`exercises`: exercises in the folder
|
||||
`test_cases`: test cases in the exercise(if it is an exercise)
|
||||
`files`: files in the exercise/folder
|
||||
"""
|
||||
|
||||
def __init__(self, url: str, soup:BeautifulSoup, session, full: bool):
|
||||
self.url = url
|
||||
self.name = soup.text
|
||||
self.__prev_raw = soup
|
||||
self.__session = session
|
||||
self.__request = self.__session.get(self.url)
|
||||
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
||||
self.__full = full
|
||||
|
||||
@property
|
||||
def am_exercise(self) -> bool:
|
||||
return "ass-submitable" in self.__prev_raw["class"]
|
||||
|
||||
# Test cases
|
||||
@property
|
||||
def test_cases(self) -> list[str]:
|
||||
section = self.__raw.find_all("div", class_="subsec round shade")
|
||||
tcs = []
|
||||
for div in section:
|
||||
res = div.find("h4", class_="info")
|
||||
if not res:
|
||||
continue
|
||||
|
||||
if "Test cases" in res.text:
|
||||
for case in div.find_all("div", class_="cfg-line"):
|
||||
if link := case.find("a"):
|
||||
tcs.append(link)
|
||||
return tcs
|
||||
|
||||
def download_tcs(self, path=".") -> list[str]:
|
||||
"""
|
||||
download_tcs(path=".") -> list[str]
|
||||
Downloads every test case available from a given exercise. `path` defaults to '.'.
|
||||
"""
|
||||
if not self.am_exercise:
|
||||
raise IllegalAction(message="You are downloading test cases from a folder.")
|
||||
|
||||
for tc in self.test_cases:
|
||||
url = f"https://themis.housing.rug.nl{tc['href']}"
|
||||
|
||||
print(f"Downloading {tc.text}")
|
||||
# download the files
|
||||
with open(f"{path}/{tc.text}", "wb") as f:
|
||||
f.write(self.__session.get(url).content)
|
||||
|
||||
return self.test_cases
|
||||
|
||||
# Files
|
||||
@property
|
||||
def files(self) -> list[str]:
|
||||
details = self.__raw.find("div", id=lambda x: x and x.startswith("details"))
|
||||
|
||||
cfg_lines = details.find_all("div", class_="cfg-line")
|
||||
|
||||
link_list = []
|
||||
|
||||
for line in cfg_lines:
|
||||
key = line.find("span", class_="cfg-key")
|
||||
|
||||
if key and "Downloads" in key.text.strip():
|
||||
# Extract all links in the cfg-val span
|
||||
links = line.find_all("span", class_="cfg-val")
|
||||
for link in links:
|
||||
a = link.find_all("a")
|
||||
for i in a:
|
||||
link_list.append(i)
|
||||
|
||||
return link_list
|
||||
|
||||
def download_files(self, path=".") -> list[str]:
|
||||
"""
|
||||
download_files(path=".") -> list[str]
|
||||
Downloads every file available from a given exercise/folder. `path` defaults to '.'.
|
||||
"""
|
||||
for file in self.files:
|
||||
print(f"Downloading file {file.text}")
|
||||
url = f"https://themis.housing.rug.nl{file['href']}"
|
||||
with open(f"{path}/{file.text}", "wb") as f:
|
||||
f.write(self.__session.get(url).content)
|
||||
return self.files
|
||||
|
||||
@property
|
||||
def exercises(self) -> list[str] | list["ExerciseGroup"]:
|
||||
if self.am_exercise:
|
||||
return self
|
||||
|
||||
section = self.__raw.find("div", class_="ass-children")
|
||||
try:
|
||||
submittables = section.find_all("a", class_="ass-submitable")
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
if not self.__full:
|
||||
return [(x.text, x["href"]) for x in submittables]
|
||||
return [
|
||||
ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{x['href']}", x, self.__session, True
|
||||
)
|
||||
for x in submittables
|
||||
]
|
||||
|
||||
@property
|
||||
def folders(self) -> list[str] | list["ExerciseGroup"]:
|
||||
section = self.__raw.find("div", class_="ass-children")
|
||||
try:
|
||||
folders = section.find_all("a", class_="ass-group")
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
if not self.__full:
|
||||
return [(x.text, x["href"]) for x in folders]
|
||||
|
||||
return [
|
||||
ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{x['href']}", x, self.__session, True
|
||||
)
|
||||
for x in folders
|
||||
]
|
||||
|
||||
# Get by name
|
||||
def get_group( # <- 🗿
|
||||
self, name: str, full: bool = False, link: str = None
|
||||
) -> "ExerciseGroup":
|
||||
"""
|
||||
get_group(name:str, full:bool=False, link:str=None) -> ExerciseGroup | list[ExerciseGroup]
|
||||
Get a single group by name.
|
||||
Set `full` to True to get all subgroups as well.
|
||||
Set `link` to directly fetch a group.
|
||||
"""
|
||||
if link:
|
||||
return ExerciseGroup(link, self.__prev_raw, self.__session, full)
|
||||
|
||||
group = self.__raw.find("a", text=name)
|
||||
if not group:
|
||||
raise IllegalAction(message=f"No such group found: {name}")
|
||||
|
||||
return ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{group['href']}", group, self.__session, full
|
||||
)
|
||||
|
||||
# Wait for result
|
||||
def __wait_for_result(self, url: str, verbose: bool, __printed: list) -> None:
|
||||
# This waits for result and returns a bundled info package
|
||||
r = self.__session.get(url)
|
||||
soup = BeautifulSoup(r.text, "lxml")
|
||||
return self.__parse_table(soup, url, verbose, __printed)
|
||||
|
||||
# Account for judge
|
||||
def __race_condition(self, url: str, verbose: bool) -> None:
|
||||
self.__session.get(url.replace("submission", "judge"))
|
||||
return self.__wait_for_result(url, verbose, [])
|
||||
|
||||
def __parse_table(
|
||||
self, soup: BeautifulSoup, url: str, verbose: bool, __printed: list
|
||||
) -> dict:
|
||||
cases = soup.find_all("tr", class_="sub-casetop")
|
||||
fail_pass = {}
|
||||
i = 1
|
||||
for case in cases:
|
||||
name = case.find("td", class_="sub-casename").text
|
||||
status = case.find("td", class_="status-icon")
|
||||
|
||||
if "pending" in status.get("class"):
|
||||
return self.__race_condition(url, verbose)
|
||||
|
||||
# queued status-icon
|
||||
if "queued" in status.get("class"):
|
||||
sleep(1) # <- 🗿
|
||||
return self.__wait_for_result(url, verbose, __printed)
|
||||
|
||||
statuses = {
|
||||
"Passed": ("✅", True),
|
||||
"Wrong output": ("❌", False),
|
||||
"No status": ("🐛", None),
|
||||
"error": ("🐛", None),
|
||||
}
|
||||
|
||||
# Printing and storing
|
||||
found = False
|
||||
for k, v in statuses.items():
|
||||
if k in status.text:
|
||||
found = True
|
||||
if verbose and int(name) not in __printed:
|
||||
print(f"{name}: {v[0]}")
|
||||
fail_pass[int(name)] = v[1]
|
||||
break
|
||||
if not found:
|
||||
fail_pass[int(name)] = None
|
||||
if verbose and int(name) not in __printed:
|
||||
print(f"{name}: Unrecognized status: {status.text}")
|
||||
|
||||
__printed.append(int(name))
|
||||
i += 1
|
||||
return fail_pass
|
||||
|
||||
# Submit
|
||||
def submit(
|
||||
self, files: list, judge: bool = True, wait: bool = True, silent: bool = True
|
||||
) -> dict | None:
|
||||
"""
|
||||
submit(files:list, judge:bool=True, wait:bool=True, silent:bool=True) -> dict | None
|
||||
Submits given files to given exercise. Returns a dictionary of test cases and their status.
|
||||
Set judge to False to not judge the submission.
|
||||
Set wait to False to not wait for the result.
|
||||
Set silent to False to print the results.
|
||||
"""
|
||||
form = self.__raw.find("form")
|
||||
if not form:
|
||||
raise IllegalAction(message="You cannot submit to this assignment.")
|
||||
|
||||
url = "https://themis.housing.rug.nl" + form["action"]
|
||||
file_types = loads(form["data-suffixes"])
|
||||
if isinstance(files, str):
|
||||
temp = []
|
||||
temp.append(files)
|
||||
files = temp
|
||||
|
||||
packaged_files = []
|
||||
data = {}
|
||||
found_type = ""
|
||||
for file in files:
|
||||
for t in file_types:
|
||||
if t in file:
|
||||
found_type = file_types[t]
|
||||
break
|
||||
if not found_type:
|
||||
print("WARNING: File type not recognized")
|
||||
|
||||
with open(file, "rb") as f:
|
||||
packaged_files.append((found_type, (file, f.read())))
|
||||
|
||||
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type if found_type else "none"}
|
||||
|
||||
if not silent:
|
||||
print(f"Submitting to {self.name}")
|
||||
for file in files:
|
||||
print(f"• {file}")
|
||||
resp = self.__session.post(url, files=packaged_files, data=data)
|
||||
|
||||
if not wait or not judge:
|
||||
return resp.url if "@submissions" in resp.url else None
|
||||
|
||||
return self.__wait_for_result(resp.url, not silent, [])
|
||||
|
||||
def __status_sections(self) -> list[BeautifulSoup]:
|
||||
r = self.__session.get("https://themis.housing.rug.nl" + self.__raw.find("a", text="Status")["href"])
|
||||
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
sections = soup.find_all('section', class_=lambda class_: class_ and 'status' in class_.split())
|
||||
|
||||
return sections
|
||||
|
||||
def __parse_section(self, section:BeautifulSoup, text) -> dict[str, Submission] | dict[str, str]:
|
||||
# The section has a heading and a body. We only care about the body
|
||||
body = section.find("div", class_="sec-body") # Find the body of the section
|
||||
body = body.find("div", class_="subsec-container") # Find the subsec-container
|
||||
body = body.find("div", class_="cfg-container")
|
||||
|
||||
# Parse the cfg-container
|
||||
parsed = {}
|
||||
|
||||
# Submission instances go here
|
||||
submissions = {}
|
||||
|
||||
cfg_lines = body.find_all("div", class_="cfg-line")
|
||||
for line in cfg_lines:
|
||||
key = line.find("span", class_="cfg-key").text.strip().split("\n")[0].replace(":", "").lower()
|
||||
value = line.find("span", class_="cfg-val").text.strip()
|
||||
|
||||
# If there is a span with class tip in the key, it means that the value is a link to a submission
|
||||
if tip := line.find("span", class_="tip"):
|
||||
value = line.find("a")["href"]
|
||||
if not text:
|
||||
submissions[key.split("\n")[0].lower().replace(" ", "_")] = Submission(value, self.__session)
|
||||
parsed[key] = value
|
||||
|
||||
if text:
|
||||
return parsed
|
||||
|
||||
return (parsed, submissions)
|
||||
|
||||
# I assume that the user would usually request submissions for an assignment,
|
||||
# so I will add a default parameter to the method.
|
||||
|
||||
def get_status(self, section:list[BeautifulSoup]=None, text:bool=False) -> dict[str, Submission] | dict[str, str]:
|
||||
"""Get the available submissions for the exercise.
|
||||
Set text to True to get the text representation of the submission."""
|
||||
if not section:
|
||||
section = self.__status_sections()
|
||||
|
||||
try:
|
||||
section = section[0] # When looking at a single exercise, there is only one status section
|
||||
except IndexError as exc:
|
||||
raise IllegalAction("Invalid status") from exc
|
||||
|
||||
return self.__parse_section(section, text)
|
||||
|
||||
def get_all_statuses(self, text:bool=False) -> list[dict[str, str]] | list[dict[str, Submission]]:
|
||||
""" Parses every visible status section. """
|
||||
|
||||
# This is useless for singular exercises, but if you want the submissions for multiple exercises, you can use this.
|
||||
statuses = []
|
||||
for section in self.__status_sections():
|
||||
if parse := self.__parse_section(section, text):
|
||||
# Find name of the exercise
|
||||
name = section.find("h3").text.replace("Status: ", "").replace("\n", "").replace("\t", "")
|
||||
statuses.append((name,parse))
|
||||
return statuses
|
||||
|
||||
def find_status(self, name:str, text:bool=False) -> dict[str, Submission] | dict[str, str] | None:
|
||||
""" Find a status block for an exercise by name. """
|
||||
# Find a section which has h3 with the name
|
||||
for section in self.__status_sections():
|
||||
if section.find("h3").text.replace("Status: ", "").replace("\n", "").replace("\t", "") == name:
|
||||
return self.__parse_section(section, text)
|
84
temmies/submission.py
Normal file
84
temmies/submission.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
File to define the submission class
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class Submission:
|
||||
"""
|
||||
Submission class
|
||||
|
||||
Methods:
|
||||
test_cases: Get a dict of test cases status
|
||||
info: Submission information (in details)
|
||||
files: Get a list of uploaded files(as names)
|
||||
"""
|
||||
def __init__(self, url: str, session):
|
||||
self.url = "https://themis.housing.rug.nl" + url
|
||||
self.__session = session
|
||||
self.__request = self.__session.get(self.url)
|
||||
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
||||
self.__info = None
|
||||
|
||||
def __clean(self, text: str, value: bool = False) -> str:
|
||||
"""Clean text"""
|
||||
clean = text.replace("\t", "").replace("\n", "")
|
||||
if value:
|
||||
return clean
|
||||
return clean.replace(" ", "_").replace(":", "").lower()
|
||||
|
||||
def test_cases(self) -> dict[str, str]:
|
||||
"""Get a dict of test cases status"""
|
||||
# In the submission page, the test cases are in a div with class "sub-cases subsec round shade"
|
||||
# print(self.__raw.prettify())
|
||||
cases = self.__raw.find("div", class_=lambda x: x and "sub-cases" in x.split())
|
||||
if not cases:
|
||||
return {}
|
||||
|
||||
# The test cases are in a table in a div with class "cfg-container"
|
||||
cases = cases.find("div", class_="cfg-container")
|
||||
cases = cases.find("table")
|
||||
# For each test case, there is a tr with class sub-casetop, which contains 2 tds:
|
||||
# * a td with class "sub-case name" which is a name
|
||||
# * a td with a variable class, which is the status text
|
||||
|
||||
results = {}
|
||||
for entry in cases.find_all("tr", class_="sub-casetop"):
|
||||
name = entry.find("td", class_="sub-casename").text
|
||||
status = entry.find(
|
||||
"td", class_=lambda x: x and "status-icon" in x.split()
|
||||
).text
|
||||
results[name] = self.__clean(status)
|
||||
|
||||
return results
|
||||
|
||||
def info(self) -> dict[str, str] | None:
|
||||
"""Submission information (in details)"""
|
||||
# in div with class subsec round shade where there is an h4 with class info
|
||||
# The info is in a div with class "cfg-container"
|
||||
if self.__info:
|
||||
return self.__info
|
||||
|
||||
for div in self.__raw.find_all("div", class_="subsec round shade"):
|
||||
if h4 := div.find("h4", class_=lambda x: x and "info" in x.split()):
|
||||
if "Details" in h4.text:
|
||||
# The information is in divs with class "cfg-line"
|
||||
# With key in span with class "cfg-key" and value in span with class "cfg-value"
|
||||
info = div.find("div", class_="cfg-container")
|
||||
info = info.find_all("div", class_="cfg-line")
|
||||
return {
|
||||
self.__clean(
|
||||
key := line.find("span", class_="cfg-key").text
|
||||
):
|
||||
self.__clean(line.find("span", class_="cfg-val").text, value=True) if "Files" not in key else
|
||||
([(self.__clean(x.text), x["href"]) for x in line.find("span", class_="cfg-val").find_all("a")])
|
||||
for line in info
|
||||
}
|
||||
return None
|
||||
|
||||
def files(self) -> list[str] | None:
|
||||
"""Get a list of uploaded files in the format [(name, url)]"""
|
||||
if not self.__info:
|
||||
self.__info = self.info()
|
||||
|
||||
return self.__info.get("files", None)
|
90
temmies/themis.py
Normal file
90
temmies/themis.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""
|
||||
Main class for the Themis API
|
||||
|
||||
"""
|
||||
|
||||
import urllib3
|
||||
from requests import Session
|
||||
from bs4 import BeautifulSoup
|
||||
from .year import Year
|
||||
from .exceptions.illegal_action import IllegalAction
|
||||
|
||||
|
||||
# Disable warnings
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
|
||||
class Themis:
|
||||
"""
|
||||
login: Login to Themis
|
||||
get_year: Get a year object
|
||||
all_years: Get all years
|
||||
"""
|
||||
|
||||
def __init__(self, user: str, passwd: str):
|
||||
self.session = self.login(user, passwd)
|
||||
self.years = []
|
||||
self.url = "https://themis.housing.rug.nl/course/"
|
||||
|
||||
def login(self, user: str, passwd: str) -> Session:
|
||||
"""
|
||||
login(self, user: str, passwd: str) -> Session
|
||||
Login to Themis
|
||||
Set user to your student number and passwd to your password
|
||||
"""
|
||||
|
||||
user_agent = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chromium/80.0.3987.160 Chrome/80.0.3987.163 Safari/537.36"
|
||||
)
|
||||
|
||||
headers = {"user-agent": user_agent}
|
||||
|
||||
data = {"user": user, "password": passwd, "null": None}
|
||||
|
||||
with Session() as s:
|
||||
url = "https://themis.housing.rug.nl/log/in"
|
||||
r = s.get(url, headers=headers, verify=False)
|
||||
soup = BeautifulSoup(r.text, "lxml")
|
||||
|
||||
# get the csrf token and add it to payload
|
||||
csrf_token = soup.find("input", attrs={"name": "_csrf"})["value"]
|
||||
data["_csrf"] = csrf_token
|
||||
data["sudo"] = user.lower()
|
||||
|
||||
# Login
|
||||
r = s.post(url, data=data, headers=headers)
|
||||
|
||||
# check if login was successful
|
||||
log_out = "Welcome, logged in as" in r.text
|
||||
if not log_out:
|
||||
raise IllegalAction(message=f"Login for user {user} failed")
|
||||
|
||||
return s
|
||||
|
||||
def get_year(self, start: int, end: int) -> Year:
|
||||
"""
|
||||
get_year(self, start: int, end: int) -> Year
|
||||
Gets a year object
|
||||
Set start to the start year and end to the end year (e.g. 2023-2024)
|
||||
"""
|
||||
return Year(self.session, start, end)
|
||||
|
||||
def all_years(self) -> list[Year]:
|
||||
"""
|
||||
get_years(self, start: int, end: int) -> list[Year]
|
||||
Gets all visible years
|
||||
"""
|
||||
# All of them are in a big ul at the beginning of the page
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, "lxml")
|
||||
ul = soup.find("ul", class_="round")
|
||||
lis = ul.find_all("li", class_="large")
|
||||
years = []
|
||||
for li in lis:
|
||||
# format: 2019-2020
|
||||
year = li.a.text.split("-")
|
||||
years.append(Year(self.session, int(year[0]), int(year[1])))
|
||||
|
||||
return years # Return a list of year objects
|
66
temmies/year.py
Normal file
66
temmies/year.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
Class which represents an academic year.
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from requests import Session
|
||||
|
||||
from .course import Course
|
||||
from .exceptions.course_unavailable import CourseUnavailable
|
||||
|
||||
|
||||
# Works
|
||||
class Year:
|
||||
"""
|
||||
all_courses: Get all visible courses in a year
|
||||
get_course: Get a course by name
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session, start_year: int, end_year: int):
|
||||
self.start = start_year
|
||||
self.year = end_year
|
||||
self.url = f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
||||
self.__session = session
|
||||
|
||||
# Method to get the courses of the year
|
||||
def all_courses(self, errors: bool = True) -> list[Course]:
|
||||
"""
|
||||
all_courses(self, errors: bool = False) -> list[Course]
|
||||
Gets all visible courses in a year.
|
||||
Set errors to False to not raise an error when a course is unavailable.
|
||||
"""
|
||||
r = self.__session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, "lxml")
|
||||
lis = soup.find_all("li", class_="large")
|
||||
courses = []
|
||||
for li in lis:
|
||||
try:
|
||||
suffix = li.a["href"].replace(f"course/{self.start}-{self.year}", "")
|
||||
courses.append(
|
||||
Course(self.url + suffix, li.a.text, self.__session, self)
|
||||
)
|
||||
except CourseUnavailable as exc:
|
||||
if errors:
|
||||
raise CourseUnavailable(
|
||||
message=f"Course {li.a.text} in year {self.start}-{self.year} unavailable"
|
||||
) from exc
|
||||
|
||||
print("Error with course", li.a.text)
|
||||
continue
|
||||
|
||||
return courses
|
||||
|
||||
def get_course(self, name: str) -> Course:
|
||||
"""
|
||||
get_course(self, name: str) -> Course
|
||||
Gets a course by name.
|
||||
"""
|
||||
# Get the course
|
||||
r = self.__session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, "lxml")
|
||||
# Search by name
|
||||
course = self.url + soup.find("a", text=name)["href"].replace(
|
||||
f"course/{self.start}-{self.year}", ""
|
||||
)
|
||||
# Get the url and transform it into a course object
|
||||
return Course(url=course, name=name, session=self.__session, parent=self)
|
Reference in New Issue
Block a user