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(
|
||||
name="temmies",
|
||||
version="1.2.12",
|
||||
version="1.2.121",
|
||||
packages=find_packages(),
|
||||
description="A wrapper for the Themis website",
|
||||
long_description=l_description,
|
||||
@ -21,6 +21,7 @@ setup(
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
install_requires=[
|
||||
"urllib3",
|
||||
"requests",
|
||||
"lxml",
|
||||
"beautifulsoup4",
|
||||
|
@ -1,5 +1,8 @@
|
||||
from .themis import Themis
|
||||
"""
|
||||
Entry point for the temmies package.
|
||||
"""
|
||||
import urllib3
|
||||
from .themis import Themis
|
||||
|
||||
__all__ = ["Themis"]
|
||||
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):
|
||||
"""
|
||||
@ -25,7 +30,7 @@ class Course(Group):
|
||||
self,
|
||||
item_data.get("submitable", False),
|
||||
)
|
||||
else:
|
||||
|
||||
return Group(
|
||||
self.session,
|
||||
item_data["path"],
|
||||
|
@ -1,12 +1,15 @@
|
||||
from .group import Group
|
||||
from .submission import Submission
|
||||
"""
|
||||
Represents a submittable exercise.
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from .group import Group
|
||||
|
||||
class ExerciseGroup(Group):
|
||||
"""
|
||||
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):
|
||||
super().__init__(session, path, title, parent, submitable=submitable)
|
||||
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
|
||||
from typing import Optional, Union, Dict
|
||||
from .exceptions.illegal_action import IllegalAction
|
||||
from .submission import Submission
|
||||
from json import loads
|
||||
from time import sleep
|
||||
from bs4 import BeautifulSoup
|
||||
from .submission import Submission
|
||||
|
||||
|
||||
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):
|
||||
self.session = session
|
||||
self.path = path # e.g., '/2023-2024/adinc-ai/labs'
|
||||
@ -31,10 +36,10 @@ class Group:
|
||||
# Fetch the page and parse it
|
||||
response = self.session.get(group_url)
|
||||
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")
|
||||
|
||||
|
||||
def get_items(self) -> list:
|
||||
"""
|
||||
Get all items (groups and assignments) under this group.
|
||||
@ -70,19 +75,20 @@ class Group:
|
||||
return item
|
||||
raise ValueError(f"Item '{title}' not found under {self.title}.")
|
||||
|
||||
|
||||
def get_status(self, text: bool = False) -> Union[Dict[str, Union[str, 'Submission']], None]:
|
||||
"""
|
||||
Get the status of the current group, if available.
|
||||
"""
|
||||
status_link = self._raw.find("a", text="Status")
|
||||
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']}"
|
||||
response = self.session.get(status_url)
|
||||
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")
|
||||
section = soup.find("div", class_="cfg-container")
|
||||
@ -92,7 +98,10 @@ class Group:
|
||||
|
||||
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.
|
||||
"""
|
||||
@ -113,8 +122,10 @@ class Group:
|
||||
continue
|
||||
|
||||
# Normalize key
|
||||
raw_key = " ".join(key_element.get_text(separator=" ").strip().replace(":", "").lower().split())
|
||||
key = key_mapping.get(raw_key, raw_key) # Use mapped key if available
|
||||
raw_key = " ".join(key_element.get_text(
|
||||
separator=" ").strip().replace(":", "").lower().split())
|
||||
# Use mapped key if available
|
||||
key = key_mapping.get(raw_key, raw_key)
|
||||
|
||||
# Process value
|
||||
link = value_element.find("a", href=True)
|
||||
@ -124,7 +135,8 @@ class Group:
|
||||
if href.startswith("/"):
|
||||
submission_url = href
|
||||
elif href.startswith("http"):
|
||||
submission_url = href.replace("https://themis.housing.rug.nl", "")
|
||||
submission_url = href.replace(
|
||||
"https://themis.housing.rug.nl", "")
|
||||
else:
|
||||
print(f"Invalid href '{href}' found in status page.")
|
||||
continue # Skip this entry if href is invalid
|
||||
@ -137,13 +149,13 @@ class Group:
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def get_test_cases(self) -> list[Dict[str, str]]:
|
||||
"""
|
||||
Get all test cases for this assignment.
|
||||
"""
|
||||
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")
|
||||
tcs = []
|
||||
@ -182,7 +194,8 @@ class Group:
|
||||
"""
|
||||
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:
|
||||
return []
|
||||
|
||||
@ -221,13 +234,21 @@ class Group:
|
||||
print(f"Failed to download file '{file['title']}'")
|
||||
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.
|
||||
Returns a dictionary of test case results or None if wait is False.
|
||||
"""
|
||||
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")
|
||||
if not form:
|
||||
@ -322,18 +343,17 @@ class Group:
|
||||
case_number = int(name)
|
||||
fail_pass[case_number] = None
|
||||
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)
|
||||
|
||||
# Polling
|
||||
# FIXME: Use ws
|
||||
# Polling (fix, use ws)
|
||||
if any_queued:
|
||||
sleep(1)
|
||||
return self.__wait_for_result(url, verbose, __printed)
|
||||
|
||||
return fail_pass
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Group({self.title}, submitable={self.submitable})"
|
||||
|
@ -82,13 +82,22 @@ class Submission:
|
||||
|
||||
# Deprecated methods
|
||||
def info(self):
|
||||
"""
|
||||
Deprecated method. Use get_info instead.
|
||||
"""
|
||||
print("This method is deprecated and will be deleted soon. Use get_info instead.")
|
||||
return self.get_info()
|
||||
|
||||
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.")
|
||||
return self.get_test_cases()
|
||||
|
||||
def files(self):
|
||||
"""
|
||||
Deprecated method. Use get_files instead.
|
||||
"""
|
||||
print("This method is deprecated and will be deleted in soon. Use get_files instead.")
|
||||
return self.get_files()
|
||||
|
@ -2,12 +2,12 @@
|
||||
Main class for the Themis API using the new JSON endpoints.
|
||||
"""
|
||||
|
||||
import keyring
|
||||
import getpass
|
||||
import keyring
|
||||
from requests import Session
|
||||
from bs4 import BeautifulSoup
|
||||
from .year import Year
|
||||
from .exceptions.illegal_action import IllegalAction
|
||||
|
||||
|
||||
class Themis:
|
||||
"""
|
||||
@ -34,6 +34,7 @@ class Themis:
|
||||
self.password = self.__get_password()
|
||||
self.base_url = "https://themis.housing.rug.nl"
|
||||
self.session = self.login(self.user, self.password)
|
||||
|
||||
def __get_password(self) -> str:
|
||||
"""
|
||||
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)
|
||||
if not password:
|
||||
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)
|
||||
print("Password saved securely in keyring.")
|
||||
return password
|
||||
@ -85,7 +87,8 @@ class Themis:
|
||||
passwd = getpass.getpass(prompt="Enter password: ")
|
||||
keyring.set_password(f'{self.user}-temmies', self.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.")
|
||||
|
||||
return session
|
||||
|
@ -1,5 +1,11 @@
|
||||
from .course import Course
|
||||
"""
|
||||
This module defines the Year class for managing academic year courses.
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from .course import Course
|
||||
|
||||
|
||||
class Year:
|
||||
"""
|
||||
Represents an academic year.
|
||||
@ -37,8 +43,6 @@ class Year:
|
||||
return course
|
||||
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:
|
||||
"""
|
||||
Gets a course by its tag (course identifier).
|
||||
@ -49,18 +53,21 @@ class Year:
|
||||
|
||||
response = self.session.get(course_url)
|
||||
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")
|
||||
|
||||
title_elements = soup.find_all("a", class_="fill accent large")
|
||||
if title_elements:
|
||||
title_element = title_elements[-1]
|
||||
title_element = title_elements[-1] if title_elements else None
|
||||
|
||||
if title_element:
|
||||
course_title = title_element.get_text(strip=True)
|
||||
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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user