Moved most logic to base Group class

This commit is contained in:
Boyan 2024-11-18 19:58:17 +01:00
parent 514fcd2438
commit 9f99df54d8

View File

@ -1,209 +1,51 @@
from .group import Group
from .exceptions.illegal_action import IllegalAction
from .submission import Submission
from json import loads
from time import sleep
from typing import Optional
from bs4 import BeautifulSoup
class ExerciseGroup(Group):
"""
Represents a group of exercises or a single exercise.
Represents a submittable exercise.
"""
def __init__(self, url: str, name: str, session, parent=None, full: bool = False, classes=None):
super().__init__(url, name, session, parent=parent, full=full, classes=classes)
self.am_exercise = "ass-submitable" in self.classes
def create_group(self, url: str, name: str, session, parent, full: bool, classes=None):
def __init__(self, session, path: str, title: str, parent, submitable: bool = True):
super().__init__(session, path, title, parent, submitable=submitable)
self.submit_url = f"{self.base_url}/api/submit{self.path}"
self.__find_name()
def __find_name(self):
"""
Create an instance of ExerciseGroup for subgroups.
Find the name of the exercise group.
"""
return ExerciseGroup(url, name, session, parent, full, classes)
@classmethod
def create_group_from_url(cls, url: str, full: bool) -> 'ExerciseGroup':
"""
Create an instance of ExerciseGroup from a full URL of a Themis group.
This method will retrieve the name of the group from the URL.
Args:
url (str): URL of the Themis group.
full (bool): Whether to traverse the whole group.
Returns:
ExerciseGroup: An instance of ExerciseGroup.
"""
if "https://themis.housing.rug.nl/course/" not in url:
url = "https://themis.housing.rug.nl/course/" + url
# Find name of group (last of a with class fill accent large)
r = cls._session.get(url)
soup = BeautifulSoup(r.text, "lxml")
group_links = soup.find_all("a", class_="fill accent large")
name = group_links[-1].text
return cls(url, name, cls._session, parent=None, full=full)
@property
def test_cases(self) -> list[str]:
"""
Get all test cases for this exercise.
"""
if not self.am_exercise:
return []
sections = self._raw.find_all("div", class_="subsec round shade")
tcs = []
for div in sections:
res = div.find("h4", class_="info")
if res and "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 all test cases for this exercise.
"""
if not self.am_exercise:
raise IllegalAction(
"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}")
with open(f"{path}/{tc.text}", "wb") as f:
f.write(self._session.get(url).content)
return self.test_cases
@property
def files(self) -> list[str]:
"""
Get all downloadable files for this exercise or group.
"""
details = self._raw.find(
"div", id=lambda x: x and x.startswith("details"))
if not details:
return []
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():
links = line.find_all("span", class_="cfg-val")
for link in links:
a_tags = link.find_all("a")
for a in a_tags:
link_list.append(a)
return link_list
def download_files(self, path=".") -> list[str]:
"""
Download all files available for this exercise or group.
"""
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
def submit(self, files: list[str], judge: bool = True, wait: bool = True, silent: bool = True) -> Optional[dict]:
if self.title == "":
# Find using beautiful soup (it is the last a with class 'fill accent large')
response = self.session.get(self.base_url + self.path)
soup = BeautifulSoup(response.text, "lxml")
title_elements = soup.find_all("a", class_="fill accent large")
if title_elements:
self.title = title_elements[-1].get_text(strip=True)
else:
self.title = self.path.split("/")[-1]
def submit(self, files: list[str]) -> Submission:
"""
Submit files to this exercise.
Returns a dictionary of test case results or None if wait is False.
"""
if not self.am_exercise:
raise IllegalAction("You cannot submit to this assignment.")
if not self.submitable:
raise ValueError(f"Cannot submit to non-submittable item '{self.title}'.")
form = self._raw.find("form")
if not form:
raise IllegalAction("Submission form not found.")
# Prepare the files and data for submission
files_payload = {}
for idx, file_path in enumerate(files):
file_key = f"file{idx}"
with open(file_path, "rb") as f:
files_payload[file_key] = (file_path, f.read())
url = "https://themis.housing.rug.nl" + form["action"]
file_types = loads(form["data-suffixes"])
response = self.session.post(self.submit_url, files=files_payload)
if response.status_code != 200:
raise ConnectionError(f"Failed to submit to '{self.title}'.")
if isinstance(files, str):
files = [files]
submission_data = response.json()
return Submission(self.session, submission_data)
packaged_files = []
data = {}
found_type = ""
for file in files:
for suffix, lang in file_types.items():
if file.endswith(suffix):
found_type = lang
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 __wait_for_result(self, url: str, verbose: bool, __printed: list) -> dict:
"""
Wait for the submission result and return the test case results.
"""
r = self._session.get(url)
soup = BeautifulSoup(r.text, "lxml")
return self.__parse_table(soup, url, verbose, __printed)
def __parse_table(self, soup: BeautifulSoup, url: str, verbose: bool, __printed: list) -> dict:
"""
Parse the results table from the submission result page.
"""
cases = soup.find_all("tr", class_="sub-casetop")
fail_pass = {}
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"):
sleep(1)
return self.__wait_for_result(url, verbose, __printed)
statuses = {
"Passed": ("", True),
"Wrong output": ("", False),
"No status": ("🐛", None),
"error": ("🐛", None),
}
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))
return fail_pass
def __str__(self):
return f"ExerciseGroup({self.title})"