reformatted to make pylint happy

TODO: get rid of the ignore comments - just fix it
This commit is contained in:
Boyan 2024-12-02 18:21:54 +01:00
parent 060e5df43e
commit 96488ac69c
8 changed files with 100 additions and 49 deletions

View File

@ -5,7 +5,7 @@ with open("README.md", "r") as f:
setup( setup(
name="temmies", name="temmies",
version="1.2.12", version="1.2.121",
packages=find_packages(), packages=find_packages(),
description="A wrapper for the Themis website", description="A wrapper for the Themis website",
long_description=l_description, long_description=l_description,
@ -21,6 +21,7 @@ setup(
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
], ],
install_requires=[ install_requires=[
"urllib3",
"requests", "requests",
"lxml", "lxml",
"beautifulsoup4", "beautifulsoup4",

View File

@ -1,5 +1,8 @@
from .themis import Themis """
Entry point for the temmies package.
"""
import urllib3 import urllib3
from .themis import Themis
__all__ = ["Themis"] __all__ = ["Themis"]
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

View File

@ -1,5 +1,10 @@
from .group import Group """
Represents a course.
A course is a group that contains exercises or other groups.
"""
from .group import Group
from .exercise_group import ExerciseGroup
class Course(Group): class Course(Group):
""" """
@ -25,11 +30,11 @@ class Course(Group):
self, self,
item_data.get("submitable", False), item_data.get("submitable", False),
) )
else:
return Group( return Group(
self.session, self.session,
item_data["path"], item_data["path"],
item_data["title"], item_data["title"],
self, self,
item_data.get("submitable", False), item_data.get("submitable", False),
) )

View File

@ -1,12 +1,15 @@
from .group import Group """
from .submission import Submission Represents a submittable exercise.
"""
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from .group import Group
class ExerciseGroup(Group): class ExerciseGroup(Group):
""" """
Represents a submittable exercise. Represents a submittable exercise.
""" """
# pylint: disable=too-many-arguments, too-many-positional-arguments
def __init__(self, session, path: str, title: str, parent, submitable: bool = True): def __init__(self, session, path: str, title: str, parent, submitable: bool = True):
super().__init__(session, path, title, parent, submitable=submitable) super().__init__(session, path, title, parent, submitable=submitable)
self.submit_url = f"{self.base_url}/api/submit{self.path}" self.submit_url = f"{self.base_url}/api/submit{self.path}"

View File

@ -1,17 +1,22 @@
from bs4 import BeautifulSoup """
from requests import Session Abstract-ish Group class for Themis API.
"""
import os import os
from typing import Optional, Union, Dict from typing import Optional, Union, Dict
from .exceptions.illegal_action import IllegalAction
from .submission import Submission
from json import loads from json import loads
from time import sleep from time import sleep
from bs4 import BeautifulSoup
from .submission import Submission
class Group: class Group:
""" """
Represents an item in Themis, which can be either a folder (non-submittable) or an assignment (submittable). Represents an item in Themis.
Can be either a folder (non-submittable) or an assignment (submittable).
""" """
# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-positional-arguments
def __init__(self, session, path: str, title: str, parent=None, submitable: bool = False): def __init__(self, session, path: str, title: str, parent=None, submitable: bool = False):
self.session = session self.session = session
self.path = path # e.g., '/2023-2024/adinc-ai/labs' self.path = path # e.g., '/2023-2024/adinc-ai/labs'
@ -31,10 +36,10 @@ class Group:
# Fetch the page and parse it # Fetch the page and parse it
response = self.session.get(group_url) response = self.session.get(group_url)
if response.status_code != 200: if response.status_code != 200:
raise ConnectionError(f"Failed to retrieve page for '{self.title}'. Tried {group_url}") raise ConnectionError(f"Failed to retrieve page for '{
self.title}'. Tried {group_url}")
self._raw = BeautifulSoup(response.text, "lxml") self._raw = BeautifulSoup(response.text, "lxml")
def get_items(self) -> list: def get_items(self) -> list:
""" """
Get all items (groups and assignments) under this group. Get all items (groups and assignments) under this group.
@ -70,19 +75,20 @@ class Group:
return item return item
raise ValueError(f"Item '{title}' not found under {self.title}.") raise ValueError(f"Item '{title}' not found under {self.title}.")
def get_status(self, text: bool = False) -> Union[Dict[str, Union[str, 'Submission']], None]: def get_status(self, text: bool = False) -> Union[Dict[str, Union[str, 'Submission']], None]:
""" """
Get the status of the current group, if available. Get the status of the current group, if available.
""" """
status_link = self._raw.find("a", text="Status") status_link = self._raw.find("a", text="Status")
if not status_link: if not status_link:
raise ValueError("Status information is not available for this group.") raise ValueError(
"Status information is not available for this group.")
status_url = f"{self.base_url}{status_link['href']}" status_url = f"{self.base_url}{status_link['href']}"
response = self.session.get(status_url) response = self.session.get(status_url)
if response.status_code != 200: if response.status_code != 200:
raise ConnectionError(f"Failed to retrieve status page for '{self.title}'.") raise ConnectionError(
f"Failed to retrieve status page for '{self.title}'.")
soup = BeautifulSoup(response.text, "lxml") soup = BeautifulSoup(response.text, "lxml")
section = soup.find("div", class_="cfg-container") section = soup.find("div", class_="cfg-container")
@ -92,7 +98,10 @@ class Group:
return self.__parse_status_section(section, text) return self.__parse_status_section(section, text)
def __parse_status_section(self, section: BeautifulSoup, text: bool) -> Dict[str, Union[str, 'Submission']]: def __parse_status_section(
self, section: BeautifulSoup,
text: bool
) -> Dict[str, Union[str, 'Submission']]:
""" """
Parse the status section of the group and clean up keys. Parse the status section of the group and clean up keys.
""" """
@ -113,8 +122,10 @@ class Group:
continue continue
# Normalize key # Normalize key
raw_key = " ".join(key_element.get_text(separator=" ").strip().replace(":", "").lower().split()) raw_key = " ".join(key_element.get_text(
key = key_mapping.get(raw_key, raw_key) # Use mapped key if available separator=" ").strip().replace(":", "").lower().split())
# Use mapped key if available
key = key_mapping.get(raw_key, raw_key)
# Process value # Process value
link = value_element.find("a", href=True) link = value_element.find("a", href=True)
@ -124,7 +135,8 @@ class Group:
if href.startswith("/"): if href.startswith("/"):
submission_url = href submission_url = href
elif href.startswith("http"): elif href.startswith("http"):
submission_url = href.replace("https://themis.housing.rug.nl", "") submission_url = href.replace(
"https://themis.housing.rug.nl", "")
else: else:
print(f"Invalid href '{href}' found in status page.") print(f"Invalid href '{href}' found in status page.")
continue # Skip this entry if href is invalid continue # Skip this entry if href is invalid
@ -137,13 +149,13 @@ class Group:
return parsed return parsed
def get_test_cases(self) -> list[Dict[str, str]]: def get_test_cases(self) -> list[Dict[str, str]]:
""" """
Get all test cases for this assignment. Get all test cases for this assignment.
""" """
if not self.submitable: if not self.submitable:
raise ValueError(f"No test cases for non-submittable item '{self.title}'.") raise ValueError(
f"No test cases for non-submittable item '{self.title}'.")
sections = self._raw.find_all("div", class_="subsec round shade") sections = self._raw.find_all("div", class_="subsec round shade")
tcs = [] tcs = []
@ -182,7 +194,8 @@ class Group:
""" """
Get all downloadable files for this assignment. Get all downloadable files for this assignment.
""" """
details = self._raw.find("div", id=lambda x: x and x.startswith("details")) details = self._raw.find(
"div", id=lambda x: x and x.startswith("details"))
if not details: if not details:
return [] return []
@ -221,13 +234,21 @@ class Group:
print(f"Failed to download file '{file['title']}'") print(f"Failed to download file '{file['title']}'")
return downloaded return downloaded
def submit(self, files: list[str], judge: bool = True, wait: bool = True, silent: bool = True) -> Optional[dict]: # pylint: disable=too-many-locals
def submit(
self,
files: list[str],
judge: bool = True,
wait: bool = True,
silent: bool = True
) -> Optional[dict]:
""" """
Submit files to this assignment. Submit files to this assignment.
Returns a dictionary of test case results or None if wait is False. Returns a dictionary of test case results or None if wait is False.
""" """
if not self.submitable: if not self.submitable:
raise ValueError(f"Cannot submit to non-submittable item '{self.title}'.") raise ValueError(
f"Cannot submit to non-submittable item '{self.title}'.")
form = self._raw.find("form") form = self._raw.find("form")
if not form: if not form:
@ -322,18 +343,17 @@ class Group:
case_number = int(name) case_number = int(name)
fail_pass[case_number] = None fail_pass[case_number] = None
if verbose and case_number not in __printed: if verbose and case_number not in __printed:
print(f"{case_number}: Unrecognized status: {status.text.strip()}") print(f"{case_number}: Unrecognized status: {
status.text.strip()}")
__printed.append(case_number) __printed.append(case_number)
# Polling # Polling (fix, use ws)
# FIXME: Use ws
if any_queued: if any_queued:
sleep(1) sleep(1)
return self.__wait_for_result(url, verbose, __printed) return self.__wait_for_result(url, verbose, __printed)
return fail_pass return fail_pass
def __str__(self): def __str__(self):
return f"Group({self.title}, submitable={self.submitable})" return f"Group({self.title}, submitable={self.submitable})"

View File

@ -82,13 +82,22 @@ class Submission:
# Deprecated methods # Deprecated methods
def info(self): def info(self):
"""
Deprecated method. Use get_info instead.
"""
print("This method is deprecated and will be deleted soon. Use get_info instead.") print("This method is deprecated and will be deleted soon. Use get_info instead.")
return self.get_info() return self.get_info()
def test_cases(self): def test_cases(self):
"""
Deprecated method. Use get_test_cases instead.
"""
print("This method is deprecated and will be deleted in soon. Use get_test_cases instead.") print("This method is deprecated and will be deleted in soon. Use get_test_cases instead.")
return self.get_test_cases() return self.get_test_cases()
def files(self): def files(self):
"""
Deprecated method. Use get_files instead.
"""
print("This method is deprecated and will be deleted in soon. Use get_files instead.") print("This method is deprecated and will be deleted in soon. Use get_files instead.")
return self.get_files() return self.get_files()

View File

@ -2,12 +2,12 @@
Main class for the Themis API using the new JSON endpoints. Main class for the Themis API using the new JSON endpoints.
""" """
import keyring
import getpass import getpass
import keyring
from requests import Session from requests import Session
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from .year import Year from .year import Year
from .exceptions.illegal_action import IllegalAction
class Themis: class Themis:
""" """
@ -34,6 +34,7 @@ class Themis:
self.password = self.__get_password() self.password = self.__get_password()
self.base_url = "https://themis.housing.rug.nl" self.base_url = "https://themis.housing.rug.nl"
self.session = self.login(self.user, self.password) self.session = self.login(self.user, self.password)
def __get_password(self) -> str: def __get_password(self) -> str:
""" """
Retrieve the password from the keyring, prompting the user if not found. Retrieve the password from the keyring, prompting the user if not found.
@ -41,7 +42,8 @@ class Themis:
password = keyring.get_password(f"{self.user}-temmies", self.user) password = keyring.get_password(f"{self.user}-temmies", self.user)
if not password: if not password:
print(f"Password for user '{self.user}' not found in keyring.") print(f"Password for user '{self.user}' not found in keyring.")
password = getpass.getpass(prompt=f"Enter password for {self.user}: ") password = getpass.getpass(
prompt=f"Enter password for {self.user}: ")
keyring.set_password(f"{self.user}-temmies", self.user, password) keyring.set_password(f"{self.user}-temmies", self.user, password)
print("Password saved securely in keyring.") print("Password saved securely in keyring.")
return password return password
@ -85,7 +87,8 @@ class Themis:
passwd = getpass.getpass(prompt="Enter password: ") passwd = getpass.getpass(prompt="Enter password: ")
keyring.set_password(f'{self.user}-temmies', self.user, passwd) keyring.set_password(f'{self.user}-temmies', self.user, passwd)
return self.login(user, passwd) return self.login(user, passwd)
elif "Welcome, logged in as" not in response.text:
if "Welcome, logged in as" not in response.text:
raise ValueError("Login failed for an unknown reason.") raise ValueError("Login failed for an unknown reason.")
return session return session
@ -95,7 +98,7 @@ class Themis:
Gets a Year object using the year path (e.g., 2023, 2024). Gets a Year object using the year path (e.g., 2023, 2024).
""" """
year_path = f"{start_year}-{end_year}" year_path = f"{start_year}-{end_year}"
return Year(self.session, year_path) return Year(self.session, year_path)
def all_years(self) -> list: def all_years(self) -> list:

View File

@ -1,5 +1,11 @@
from .course import Course """
This module defines the Year class for managing academic year courses.
"""
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from .course import Course
class Year: class Year:
""" """
Represents an academic year. Represents an academic year.
@ -37,8 +43,6 @@ class Year:
return course return course
raise ValueError(f"Course '{course_title}' not found in year {self.year_path}.") raise ValueError(f"Course '{course_title}' not found in year {self.year_path}.")
from bs4 import BeautifulSoup
def get_course_by_tag(self, course_tag: str) -> Course: def get_course_by_tag(self, course_tag: str) -> Course:
""" """
Gets a course by its tag (course identifier). Gets a course by its tag (course identifier).
@ -49,18 +53,21 @@ class Year:
response = self.session.get(course_url) response = self.session.get(course_url)
if response.status_code != 200: if response.status_code != 200:
raise ConnectionError(f"Failed to retrieve course with tag '{course_tag}' for year {self.year_path}. Tried {course_url}") raise ConnectionError(
f"Failed to retrieve course '{course_tag}' for year {self.year_path}."
)
soup = BeautifulSoup(response.text, "lxml") soup = BeautifulSoup(response.text, "lxml")
title_elements = soup.find_all("a", class_="fill accent large") title_elements = soup.find_all("a", class_="fill accent large")
if title_elements: title_element = title_elements[-1] if title_elements else None
title_element = title_elements[-1]
if title_element: if title_element:
course_title = title_element.get_text(strip=True) course_title = title_element.get_text(strip=True)
else: else:
raise ValueError(f"Could not retrieve course title for tag '{course_tag}' in year {self.year_path}.") raise ValueError(
f"Could not retrieve course title for tag '{course_tag}' in year {self.year_path}."
)
return Course(self.session, course_path, course_title, self) return Course(self.session, course_path, course_title, self)