mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-16 23:20: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
|
config.py
|
||||||
baller.py
|
baller.py
|
||||||
|
|
||||||
|
#Doc env
|
||||||
|
.docs_env
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
@ -3,12 +3,10 @@ version: 2
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
python: "3.12"
|
python: "3.8"
|
||||||
|
|
||||||
sphinx:
|
|
||||||
configuration: docs/conf.py
|
|
||||||
|
|
||||||
python:
|
mkdocs:
|
||||||
install:
|
configuration: mkdocs.yml
|
||||||
- requirements: docs/requirements.txt
|
|
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">
|
<p align="center">
|
||||||
<img src="images/rugemmie.gif" />
|
<img src="docs/img/rugemmie.gif" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
A python library which interacts with themis. Uses bs4. I'll try to end development on a somewhat working state.
|
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 requests import Session
|
||||||
from ExerciseGroup import ExerciseGroup
|
from ExerciseGroup import ExerciseGroup
|
||||||
import re
|
import re
|
||||||
from Base import Base
|
|
||||||
from exceptions.CourseUnavailable import CourseUnavailable
|
from exceptions.CourseUnavailable import CourseUnavailable
|
||||||
|
|
||||||
# PROBLEM: This implementation is bad due to inconsistencies in the website
|
class Course:
|
||||||
# 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):
|
|
||||||
# Extend the Base class init
|
# Extend the Base class init
|
||||||
def __init__(self, url:str, name:str, session:Session, parent):
|
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.assignments = []
|
||||||
self.__courseAvailable(self.session.get(self.url))
|
self.__courseAvailable(self.session.get(self.url))
|
||||||
|
|
||||||
@ -26,7 +22,7 @@ class Course(Base):
|
|||||||
# Check if we got an error
|
# Check if we got an error
|
||||||
# print(self.url)
|
# print(self.url)
|
||||||
if "Something went wrong" in r.text:
|
if "Something went wrong" in r.text:
|
||||||
raise CourseUnavailable()
|
raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def info(self):
|
def info(self):
|
||||||
@ -42,4 +38,11 @@ class Course(Base):
|
|||||||
soup = BeautifulSoup(r.text, 'lxml')
|
soup = BeautifulSoup(r.text, 'lxml')
|
||||||
section = soup.find('div', class_="ass-children")
|
section = soup.find('div', class_="ass-children")
|
||||||
entries = section.find_all('a', href=True)
|
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):
|
class ExerciseGroup():
|
||||||
# I can't tell if I'm already an exercise :C
|
def __init__(self, url:str, soup, session, parent):
|
||||||
|
self.url = url
|
||||||
def __init__(self, url:str, name:str, session, parent):
|
self.name = soup.text
|
||||||
super().__init__(url, name, session, parent)
|
self.__raw = soup
|
||||||
self.exercises = self.getExercises()
|
self.session = session
|
||||||
self.folders = self.getFolders()
|
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):
|
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:
|
@property
|
||||||
r = self.session.get(self.url)
|
def amExercise(self):
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
return "ass-submitable" in self.__raw['class']
|
||||||
section = soup.find('div', class_="ass-children")
|
|
||||||
|
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:
|
try:
|
||||||
submittables = section.find_all('a', class_="ass-submitable")
|
submittables = section.find_all('a', class_="ass-submitable")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
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
|
@property
|
||||||
def getFolders(self) -> list:
|
def folders(self) -> list:
|
||||||
r = self.session.get(self.url)
|
section = self.soup.find('div', class_="ass-children")
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
section = soup.find('div', class_="ass-children")
|
|
||||||
try:
|
try:
|
||||||
folders = section.find_all('a', class_="ass-group")
|
folders = section.find_all('a', class_="ass-group")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
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):
|
def getYear(self, start:int, end:int):
|
||||||
# Get the current year
|
|
||||||
return Year(self.session, self, start, end)
|
return Year(self.session, self, start, end)
|
||||||
|
|
||||||
def allYears(self):
|
def allYears(self):
|
||||||
|
@ -18,7 +18,7 @@ class Year:
|
|||||||
return f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
return f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
||||||
|
|
||||||
# Method to get the courses of the 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
|
# lis in a big ul
|
||||||
r = self.session.get(self.url)
|
r = self.session.get(self.url)
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
soup = BeautifulSoup(r.text, 'lxml')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
class CourseUnavailable(Exception):
|
class CourseUnavailable(Exception):
|
||||||
def __init__(self, message:str="Error in course"):
|
def __init__(self, message:str=""):
|
||||||
self.message = message
|
self.message = "Course Error: " + message
|
||||||
super().__init__(self.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