mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 07:10:15 +01:00
reformatted to make pylint happy
TODO: get rid of the ignore comments - just fix it
This commit is contained in:
parent
060e5df43e
commit
96488ac69c
3
setup.py
3
setup.py
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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}"
|
||||||
|
@ -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})"
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user