mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-16 15:10:16 +01:00
Compare commits
6 Commits
e3d863d7b2
...
b819305704
Author | SHA1 | Date | |
---|---|---|---|
b819305704 | |||
1016e56e70 | |||
b1eface45d | |||
b8a6e05ea0 | |||
c0056a27d1 | |||
cff77bcc95 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,7 +1,10 @@
|
||||
# Config
|
||||
# Config - Testing
|
||||
config.py
|
||||
baller.py
|
||||
|
||||
#Doc env
|
||||
.docs_env
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -3,12 +3,10 @@ version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
|
||||
tools:
|
||||
python: "3.12"
|
||||
python: "3.8"
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
30
Folder.py
30
Folder.py
@ -1,30 +0,0 @@
|
||||
# Module to handle each assignment (most difficult part)
|
||||
|
||||
from Base import Base
|
||||
from Exercise import Exercise
|
||||
from requests import Session
|
||||
|
||||
|
||||
class Assignment(Base):
|
||||
def __init__(self, url:str, name:str, session:Session, parent):
|
||||
super().__init__(url, name, session, parent)
|
||||
self.download = Downloadable(name, session, self)
|
||||
|
||||
def __str__(self):
|
||||
return f"Assignment {self.name} in course {self.parent.name}"
|
||||
|
||||
def getExercises(self) -> list[Exercise]:
|
||||
# Find li large
|
||||
ul = self.soup.find('ul', class_='round')
|
||||
|
||||
# Turn each li to an exercise instance
|
||||
return self.liLargeToExercises(ul, self.session, self)
|
||||
|
||||
def getExercise(self, name:str) -> Exercise:
|
||||
# Get the exercise
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
# Search by name
|
||||
exercise = soup.find('a', text=name)
|
||||
# Get the url and transform it into an exercise object
|
||||
return Exercise(url=exercise['href'], name=name, session=self.session, assignment=self)
|
20
Makefile
20
Makefile
@ -1,20 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="images/rugemmie.gif" />
|
||||
<img src="docs/img/rugemmie.gif" />
|
||||
</p>
|
||||
|
||||
A python library which interacts with themis. Uses bs4. I'll try to end development on a somewhat working state.
|
||||
|
10
docs/about.md
Normal file
10
docs/about.md
Normal file
@ -0,0 +1,10 @@
|
||||
# This project was made with ❤️
|
||||
By [Boyan](https://confest.im) from the student organization [Code for Groningen](https://github.com/Code-For-Groningen/).
|
||||
|
||||
It has **no** affiliation with the [University of Groningen](https://rug.nl).
|
||||
|
||||
## Contact
|
||||
Shoot me an email: boyan(plus)cfg(at)bobokara.com.
|
||||
|
||||
## License
|
||||
This project is licensed under the GPL 3.0 license.
|
141
docs/api.md
Normal file
141
docs/api.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Classes
|
||||
---
|
||||
## `Themis`
|
||||
Creates the initial connection to Themis.
|
||||
|
||||
### Usage
|
||||
```python
|
||||
from temmies.Themis import Themis
|
||||
|
||||
themis = Themis("s-number", "password")
|
||||
```
|
||||
|
||||
### Methods
|
||||
#### `login()`
|
||||
Logs in to Themis. Runs automatically when the class is initialized.
|
||||
|
||||
#### `getYear(start, end)`
|
||||
Returns an instance of a [`Year`](#year)(academic year) between `start` and `end`.
|
||||
|
||||
```python
|
||||
year = themis.getYear(2023, 2024)
|
||||
```
|
||||
|
||||
#### `allYears()`
|
||||
Returns a list of `Year` instances corresponding to all years visible to the user.
|
||||
|
||||
```python
|
||||
years = themis.allYears()
|
||||
```
|
||||
<sub> I don't see why you would need this, but it's here. </sub>
|
||||
|
||||
----
|
||||
|
||||
## `Year`
|
||||
|
||||
### Usage
|
||||
```python
|
||||
year = themis.getYear(2023, 2024)
|
||||
```
|
||||
|
||||
### Methods
|
||||
#### `getCourse(courseName)`
|
||||
Returns an instance of a [`Course`](#course) with the name `courseName`.
|
||||
|
||||
```python
|
||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
||||
```
|
||||
|
||||
#### `allCourses()`
|
||||
Returns a list of `Course` instances corresponding to all courses visible to the user in a given `Year`.
|
||||
|
||||
```python
|
||||
courses = year.allCourses()
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
## `Course`
|
||||
### Usage
|
||||
```python
|
||||
|
||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
||||
print(pf.info) # <- course info attribute
|
||||
assignments = pf.getExerciseGroups()
|
||||
```
|
||||
|
||||
### Methods
|
||||
#### `getExerciseGroups()`
|
||||
Returns a list of `ExerciseGroup` instances corresponding to all exercise groups visible to the user in a given `Course`.
|
||||
|
||||
```python
|
||||
assignments = pf.getExerciseGroups()
|
||||
```
|
||||
|
||||
## `ExerciseGroup`
|
||||
When this class is initialized, it will automatically fetch the exercise's info, files and test cases(it might be slow, because it indexes the entire course, which I will fix at some point).
|
||||
|
||||
* Both folders and exercises are represented as `ExerciseGroup` instances.
|
||||
* Folders will have the `amExercise` attribute set to `False`.
|
||||
* Folders can have the `downloadFiles` method called on them.
|
||||
* Exercises can have the `submit`, `downloadFiles` and `downloadTCs` method called on them.
|
||||
|
||||
|
||||
### Usage
|
||||
```python
|
||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
||||
assignments = pf.getExerciseGroups()
|
||||
assignment = assignments[0]
|
||||
print(assignment.amExercise) # <- Exercise or folder attribute
|
||||
print(assignment.files) # <- Downloadable files attribute
|
||||
print(assignment.testCases) # <- Test cases attribute
|
||||
|
||||
print(assignment.folders) # <- If the group contains folders, they will be here
|
||||
print(assignment.exercises) # <- If the group contains exercises, they will be here
|
||||
```
|
||||
|
||||
### Example of folder traversal
|
||||
Let's say we have a folder structure like this:
|
||||
```
|
||||
- Course Name
|
||||
- Week 1
|
||||
- Exercise 1
|
||||
- Exercise 2
|
||||
- Part 1
|
||||
- Part 2
|
||||
- Week 2
|
||||
- Exercise 1
|
||||
- Exercise 2
|
||||
```
|
||||
And we want to get to `Part 2` of `Week 1`'s `Exercise 2`. We would do this:
|
||||
|
||||
```python
|
||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
||||
assignments = pf.getExerciseGroups()
|
||||
week1 = assignments[0].folders[0]
|
||||
exercise2 = week1.exercises[1]
|
||||
part2 = exercise2.folders[1]
|
||||
```
|
||||
|
||||
|
||||
### Methods
|
||||
#### `downloadFiles(path=".")`
|
||||
Downloads all files in the exercise group to a directory `path`. Defaults to the current directory.
|
||||
|
||||
```python
|
||||
assignment.downloadFiles()
|
||||
```
|
||||
|
||||
#### `downloadTCs(path=".")`
|
||||
Downloads all test cases in the exercise group to a directory `path`. Defaults to the current directory.
|
||||
|
||||
```python
|
||||
assignment.downloadTCs()
|
||||
```
|
||||
|
||||
#### `submit(files)`
|
||||
Submits the files to the exercise group. (This is not implemented yet)
|
||||
|
||||
```python
|
||||
assignment.submit(["file1.py", "file2.py"])
|
||||
```
|
27
docs/conf.py
27
docs/conf.py
@ -1,27 +0,0 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'temmies-docs'
|
||||
copyright = '2024, Boyan K.'
|
||||
author = 'Boyan K.'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = []
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = []
|
||||
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_static_path = ['_static']
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
40
docs/index.md
Normal file
40
docs/index.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Temmies!
|
||||
<center></center>
|
||||
|
||||
|
||||
## What is this?
|
||||
A python library which interacts with themis. Uses bs4. I'll try to end development on a somewhat working state. [Check out the code](https://github.com/Code-For-Groningen/temmies)
|
||||
|
||||
## Intended Features
|
||||
* Log in
|
||||
* Bulk download of test cases and files~~
|
||||
* Submitting files
|
||||
* Somewhat easy to use API to interact with courses
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
pip install temmies
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
```python
|
||||
import temmies
|
||||
|
||||
# Log in
|
||||
themis = temmies.Themis("s-number", "password")
|
||||
|
||||
# Get a year
|
||||
year = themis.getYear(2023, 2024)
|
||||
|
||||
# Get a course
|
||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
||||
|
||||
# Get an assignment
|
||||
assignment = pf.getExerciseGroups()
|
||||
|
||||
# Download the files
|
||||
assignment.downloadFiles()
|
||||
```
|
||||
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
.. temmies-docs documentation master file, created by
|
||||
sphinx-quickstart on Tue Feb 13 20:53:28 2024.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Temmies!
|
||||
========================================
|
||||
.. image:: https://static.wikia.nocookie.net/undertale/images/7/7b/Temmie_battle_idle.gif
|
||||
:align: center
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -1 +0,0 @@
|
||||
sphinx-rtd-theme==1.3.0
|
6
mkdocs.yml
Normal file
6
mkdocs.yml
Normal file
@ -0,0 +1,6 @@
|
||||
site_name: Temmies
|
||||
nav:
|
||||
- Temmies: index.md
|
||||
- API Reference: api.md
|
||||
- About: about.md
|
||||
theme: readthedocs
|
@ -1,31 +0,0 @@
|
||||
# Module to handle each assignment (most difficult part)
|
||||
|
||||
from Downloadable import Downloadable
|
||||
from Base import Base
|
||||
from Exercise import Exercise
|
||||
from requests import Session
|
||||
|
||||
|
||||
class Assignment(Base):
|
||||
def __init__(self, url:str, name:str, session:Session, parent):
|
||||
super().__init__(url, name, session, parent)
|
||||
self.download = Downloadable(name, session, self)
|
||||
|
||||
def __str__(self):
|
||||
return f"Assignment {self.name} in course {self.parent.name}"
|
||||
|
||||
def getExercises(self) -> list[Exercise]:
|
||||
# Find li large
|
||||
ul = self.soup.find('ul', class_='round')
|
||||
|
||||
# Turn each li to an exercise instance
|
||||
return self.liLargeToExercises(ul, self.session, self)
|
||||
|
||||
def getExercise(self, name:str) -> Exercise:
|
||||
# Get the exercise
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
# Search by name
|
||||
exercise = soup.find('a', text=name)
|
||||
# Get the url and transform it into an exercise object
|
||||
return Exercise(url=exercise['href'], name=name, session=self.session, assignment=self)
|
72
src/Base.py
72
src/Base.py
@ -1,72 +0,0 @@
|
||||
# Noticed there's a similar pattern in the classes, so I'm going to create a base class for them
|
||||
|
||||
# classes that inherit from Base:
|
||||
# - Course
|
||||
# - Assignment
|
||||
# - Exercise
|
||||
from requests import Session
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class Base:
|
||||
def __init__(self, url:str, name:str, session:Session, parent):
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.session = session
|
||||
self.parent = parent
|
||||
|
||||
def __parseCfgBlock(self, div:BeautifulSoup) -> dict:
|
||||
# We assume that the div is a submission with class "cfg-container round"
|
||||
# Put each key and value in a dictionary
|
||||
# The key is a span with a class "cfg-key"
|
||||
# The value is a span with a class "cfg-val"
|
||||
|
||||
# Get the key and value spans
|
||||
keys = div.find_all('span', class_="cfg-key")
|
||||
values = div.find_all('span', class_="cfg-val")
|
||||
|
||||
# Create a dictionary
|
||||
submission = {}
|
||||
|
||||
# Put each key and value in the dictionary
|
||||
for i in range(len(keys)):
|
||||
submission[keys[i].text] = values[i].text
|
||||
|
||||
return submission
|
||||
|
||||
|
||||
# TODO: Fix
|
||||
def getDownloadable(self, soup) -> list:
|
||||
# Make sure we only get the ones that have a link
|
||||
# We parse the cfg and check for the key "Downloads"
|
||||
# Check if downloads are available
|
||||
print(soup)
|
||||
cfg = soup.find('div', class_='cfg-container round')
|
||||
print(cfg)
|
||||
cfg = self.__parseCfgBlock(cfg)
|
||||
# Get the downloads
|
||||
downloads = cfg.get("Downloads", None)
|
||||
if downloads == None:
|
||||
return []
|
||||
# Get the links
|
||||
links = downloads.find_all('a')
|
||||
files = []
|
||||
for link in links:
|
||||
files.append(Base(link['href'], link.text, self.session, self))
|
||||
|
||||
return files
|
||||
|
||||
def getSubmissions(self):
|
||||
# We change the url where course becomes stats
|
||||
url = self.url.replace("course", "stats")
|
||||
r = self.session.get(url)
|
||||
|
||||
# Get each div with class "cfg-container round"
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
divs = soup.find_all('div', class_="cfg-container round")
|
||||
|
||||
# The first one is an overview, the next ones are the submissions
|
||||
submissions = []
|
||||
for div in divs[1:]:
|
||||
submissions.append(self.__parseCfgBlock(div))
|
||||
return self.__parseCfgBlock(divs[0]), submissions
|
||||
|
@ -3,19 +3,15 @@ from bs4 import BeautifulSoup
|
||||
from requests import Session
|
||||
from ExerciseGroup import ExerciseGroup
|
||||
import re
|
||||
from Base import Base
|
||||
from exceptions.CourseUnavailable import CourseUnavailable
|
||||
|
||||
# PROBLEM: This implementation is bad due to inconsistencies in the website
|
||||
# The way we can tell the difference between an assignment and an exercise is by the presence of an a with the class "ass-submitable"
|
||||
# As opposed to folders which contain exercises which are marked with "ass-group"
|
||||
# Therefore, we should take that into consideration and spawn the corresponding Exercise or Assignment class
|
||||
# Naming becomes a bit inconsistent like that as well, as Assignments could be Exercises. Might opt to call the "assignments" "exerciseGroups" or some shit.
|
||||
|
||||
class Course(Base):
|
||||
class Course:
|
||||
# Extend the Base class init
|
||||
def __init__(self, url:str, name:str, session:Session, parent):
|
||||
super().__init__(url, name, session, parent)
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.session = session
|
||||
self.parent = parent
|
||||
self.assignments = []
|
||||
self.__courseAvailable(self.session.get(self.url))
|
||||
|
||||
@ -26,7 +22,7 @@ class Course(Base):
|
||||
# Check if we got an error
|
||||
# print(self.url)
|
||||
if "Something went wrong" in r.text:
|
||||
raise CourseUnavailable()
|
||||
raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ")
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
@ -42,4 +38,11 @@ class Course(Base):
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
section = soup.find('div', class_="ass-children")
|
||||
entries = section.find_all('a', href=True)
|
||||
return [ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", x.text, self.session, self) for x in entries]
|
||||
return [
|
||||
ExerciseGroup(
|
||||
f"https://themis.housing.rug.nl{x['href']}",
|
||||
x,
|
||||
self.session,
|
||||
self,
|
||||
)
|
||||
for x in entries]
|
@ -1,45 +0,0 @@
|
||||
# Since we can download files both from the assignment itself and its exercises, this class will handle both
|
||||
|
||||
from requests import Session
|
||||
from bs4 import BeautifulSoup
|
||||
from Base import Base
|
||||
|
||||
class Downloadable(Base):
|
||||
def __init__(self, name, session:Session, parent):
|
||||
self.name = name
|
||||
self.session = session
|
||||
self.parent = parent
|
||||
|
||||
# File handling
|
||||
def __findFile(self, name:str):
|
||||
# Get the file by name
|
||||
for file in self.files:
|
||||
if file.name == name:
|
||||
return file
|
||||
return None
|
||||
|
||||
@property
|
||||
def files(self) -> list:
|
||||
# Create a list of files
|
||||
# They are all links in a span with class "cfg-val"
|
||||
r = self.session.get("https://themis.housing.rug.nl" + self.parent.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
return self.getDownloadable(soup)
|
||||
|
||||
def download(self, filename:str) -> str:
|
||||
# Download the file
|
||||
if filename == None:
|
||||
raise NameError("No filename provided")
|
||||
|
||||
file = self.__findFile(filename)
|
||||
r = self.session.get(file.url, stream=True)
|
||||
with open(file.name, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
return file.name
|
||||
|
||||
def downloadAll(self) -> list[str]:
|
||||
# Download all files
|
||||
return [self.download(file.name) for file in self.files]
|
||||
|
@ -1,73 +0,0 @@
|
||||
from Base import Base
|
||||
from Downloadable import Downloadable
|
||||
from requests import Session
|
||||
|
||||
from time import sleep
|
||||
|
||||
|
||||
class Exercise(Base):
|
||||
def __init__(self, url:str, name:str, session:Session, parent):
|
||||
super().__init__()
|
||||
self.download = Downloadable(url, name, session, self)
|
||||
|
||||
def __str__(self):
|
||||
return f"Exercise {self.name} in assignment {self.parent.name}"
|
||||
|
||||
def getTests(self) -> list[str]:
|
||||
pass
|
||||
|
||||
def submit(self, file:str, comment:str) -> str:
|
||||
# Submit a file
|
||||
# The form is in the page with class "cfg-container round"
|
||||
# The form is a POST request to the url with the file and the comment
|
||||
# The url looks like this: https://themis.housing.rug.nl/submit/{year}/{course}/{assignment}/{exercise}?_csrf={session_csrf}&sudo={username}
|
||||
# The current url looks like: https://themis.housing.rug.nl/course/{year}/{course}/{assignment}/{exercise}
|
||||
# The request should contain the contents of the file
|
||||
|
||||
# Get the url
|
||||
url = self.url.replace("course", "submit")
|
||||
# Get the csrf token
|
||||
csrf = self.session.cookies['_csrf']
|
||||
# Get the username
|
||||
username = self.session.cookies['username']
|
||||
|
||||
# Open the file
|
||||
with open(file, 'rb') as f:
|
||||
# Submit the file
|
||||
# After submission it will 302 to the current submission page
|
||||
r = self.session.post(url, files={'file': f}, data={'comment': comment, '_csrf': csrf, 'sudo': username})
|
||||
|
||||
# Follow the redirect and repeatedly send get requests to the page
|
||||
|
||||
# We have a table which represents the test cases. The program should wait until all the test cases are done
|
||||
# The test case is done when all of the elements in the table are not none
|
||||
# The element which showcases this for each <tr class="sub-casetop">
|
||||
# is the class in there. if it is "queued" it is still running.
|
||||
|
||||
# Get the url
|
||||
url = r.url
|
||||
# Get the page
|
||||
r = self.session.get(url)
|
||||
# Get the soup
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
# Get the table
|
||||
table = soup.find('table')
|
||||
# Get the rows
|
||||
rows = table.find_all('tr', class_='sub-casetop')
|
||||
# Get the status
|
||||
status = [row.find('td', class_='status').text for row in rows]
|
||||
# Wait until all the status are not queued
|
||||
while "queued" in status:
|
||||
# Wait a bit
|
||||
sleep(1)
|
||||
# Get the page
|
||||
r = self.session.get(url)
|
||||
# Get the soup
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
# Get the table
|
||||
table = soup.find('table')
|
||||
# Get the rows
|
||||
rows = table.find_all('tr', class_='sub-casetop')
|
||||
|
||||
|
||||
pass
|
@ -1,39 +1,126 @@
|
||||
from Base import Base
|
||||
from bs4 import BeautifulSoup\
|
||||
from bs4 import BeautifulSoup
|
||||
from exceptions.IllegalAction import IllegalAction
|
||||
import re
|
||||
|
||||
class ExerciseGroup(Base):
|
||||
# I can't tell if I'm already an exercise :C
|
||||
|
||||
def __init__(self, url:str, name:str, session, parent):
|
||||
super().__init__(url, name, session, parent)
|
||||
self.exercises = self.getExercises()
|
||||
self.folders = self.getFolders()
|
||||
class ExerciseGroup():
|
||||
def __init__(self, url:str, soup, session, parent):
|
||||
self.url = url
|
||||
self.name = soup.text
|
||||
self.__raw = soup
|
||||
self.session = session
|
||||
self.parent = parent # This is unnecessary, but I'll keep it for now
|
||||
self.request = self.session.get(self.url)
|
||||
self.soup = BeautifulSoup(self.request.text, 'lxml')
|
||||
|
||||
def __str__(self):
|
||||
return f"ExerciseGroup {self.name} in course {self.parent.name}"
|
||||
return f"ExerciseGroup {self.name} in folder {self.parent.name}"
|
||||
|
||||
def getExercises(self) -> list:
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
section = soup.find('div', class_="ass-children")
|
||||
@property
|
||||
def amExercise(self):
|
||||
return "ass-submitable" in self.__raw['class']
|
||||
|
||||
def submit(self):
|
||||
if not self.amExercise:
|
||||
raise IllegalAction(message="You are submitting to a folder.")
|
||||
|
||||
# Logic for submitting
|
||||
|
||||
# Test cases
|
||||
@property
|
||||
def testCases(self):
|
||||
section = self.soup.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
|
||||
return None
|
||||
|
||||
def downloadTCs(self, path="."):
|
||||
# Logic for downloading test cases(if any)
|
||||
# In a div with class "subsec round shade", where there is an h4 with text "Test cases"
|
||||
if not self.amExercise:
|
||||
raise IllegalAction(message="You are downloading test cases from a folder.")
|
||||
|
||||
for tc in self.testCases:
|
||||
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.testCases
|
||||
|
||||
# Files
|
||||
@property
|
||||
def files(self):
|
||||
details = self.soup.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 a in a:
|
||||
link_list.append(a)
|
||||
|
||||
return link_list if link_list else None
|
||||
|
||||
|
||||
|
||||
def downloadFiles(self, path="."):
|
||||
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
|
||||
|
||||
# idea exercises and folders are identical, maybe merge them?
|
||||
@property
|
||||
def exercises(self) -> list:
|
||||
if self.amExercise:
|
||||
return self
|
||||
|
||||
section = self.soup.find('div', class_="ass-children")
|
||||
try:
|
||||
submittables = section.find_all('a', class_="ass-submitable")
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
return submittables
|
||||
return [
|
||||
ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}",
|
||||
x,
|
||||
self.session,
|
||||
self)
|
||||
for x in submittables]
|
||||
|
||||
# Returns a list of names of the folders
|
||||
def getFolders(self) -> list:
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
section = soup.find('div', class_="ass-children")
|
||||
@property
|
||||
def folders(self) -> list:
|
||||
section = self.soup.find('div', class_="ass-children")
|
||||
try:
|
||||
folders = section.find_all('a', class_="ass-group")
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
return [x.text for x in folders]
|
||||
return [
|
||||
ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}",
|
||||
x,
|
||||
session,
|
||||
self)
|
||||
for x in folders]
|
||||
|
||||
def recurse(self, folder:str):
|
||||
print(self.url)
|
@ -44,7 +44,6 @@ class Themis:
|
||||
|
||||
|
||||
def getYear(self, start:int, end:int):
|
||||
# Get the current year
|
||||
return Year(self.session, self, start, end)
|
||||
|
||||
def allYears(self):
|
||||
|
@ -18,7 +18,7 @@ class Year:
|
||||
return f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
||||
|
||||
# Method to get the courses of the year
|
||||
def getCourses(self, errors:bool=False) -> list[Course]:
|
||||
def allCourses(self, errors:bool=False) -> list[Course]:
|
||||
# lis in a big ul
|
||||
r = self.session.get(self.url)
|
||||
soup = BeautifulSoup(r.text, 'lxml')
|
||||
|
@ -1,4 +1,4 @@
|
||||
class CourseUnavailable(Exception):
|
||||
def __init__(self, message:str="Error in course"):
|
||||
self.message = message
|
||||
def __init__(self, message:str=""):
|
||||
self.message = "Course Error: " + message
|
||||
super().__init__(self.message)
|
4
src/exceptions/IllegalAction.py
Normal file
4
src/exceptions/IllegalAction.py
Normal file
@ -0,0 +1,4 @@
|
||||
class IllegalAction(Exception):
|
||||
def __init__(self, message:str=""):
|
||||
self.message = "Illegal action: " + message
|
||||
super().__init__(self.message)
|
Loading…
x
Reference in New Issue
Block a user