mirror of
https://github.com/Code-For-Groningen/temmies.git
synced 2025-03-15 15:10:15 +01:00
Compare commits
5 Commits
11864cae6b
...
de6262a4bb
Author | SHA1 | Date | |
---|---|---|---|
de6262a4bb | |||
8325a30af8 | |||
0f92d05bec | |||
2785ec86e9 | |||
78aade7c8c |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
# Config - Testing
|
# Config - Testing
|
||||||
config.py
|
config.py
|
||||||
tests/
|
tests/
|
||||||
|
pathfinding/
|
||||||
|
test.py
|
||||||
|
|
||||||
#Doc env
|
#Doc env
|
||||||
.docs_env
|
.docs_env
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/img/rugemmie.gif" />
|
<img src="docs/img/rugemmie.gif" />
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://temmies.readthedocs.io/en/latest/"><img alt="Read the Docs" src="https://img.shields.io/readthedocs/temmies"></a>
|
||||||
|
<img alt="GitHub" src="https://img.shields.io/github/license/Code-For-Groningen/temmies">
|
||||||
|
</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.
|
||||||
|
|
||||||
@ -9,14 +13,10 @@ A python library which interacts with themis. Uses bs4. I'll try to end developm
|
|||||||
* [x] Submit
|
* [x] Submit
|
||||||
* [x] Bulk download of test cases and files
|
* [x] Bulk download of test cases and files
|
||||||
* [ ] Submission status
|
* [ ] Submission status
|
||||||
* [ ] Classes, methods and attributes described in the map below
|
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
[here](http://temmies.rtfd.io/).
|
[here](http://temmies.rtfd.io/).
|
||||||
|
|
||||||
## Class map
|
|
||||||

|
|
||||||
|
|
||||||
## Possible continuations
|
## Possible continuations
|
||||||
* [ ] Discord bot
|
* [ ] Discord bot
|
||||||
* [ ] CLI program
|
* [ ] CLI program
|
||||||
|
79
docs/api.md
79
docs/api.md
@ -14,18 +14,18 @@ themis = Themis("s-number", "password")
|
|||||||
#### `login()`
|
#### `login()`
|
||||||
Logs in to Themis. Runs automatically when the class is initialized.
|
Logs in to Themis. Runs automatically when the class is initialized.
|
||||||
|
|
||||||
#### `getYear(start, end)`
|
#### `get_year(start, end)`
|
||||||
Returns an instance of a [`Year`](#year)(academic year) between `start` and `end`.
|
Returns an instance of a [`Year`](#year)(academic year) between `start` and `end`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
year = themis.getYear(2023, 2024)
|
year = themis.get_year(2023, 2024)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `allYears()`
|
#### `all_years()`
|
||||||
Returns a list of `Year` instances corresponding to all years visible to the user.
|
Returns a list of `Year` instances corresponding to all years visible to the user.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
years = themis.allYears()
|
years = themis.all_years()
|
||||||
```
|
```
|
||||||
<sub> I don't see why you would need this, but it's here. </sub>
|
<sub> I don't see why you would need this, but it's here. </sub>
|
||||||
|
|
||||||
@ -35,22 +35,22 @@ years = themis.allYears()
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
```python
|
```python
|
||||||
year = themis.getYear(2023, 2024)
|
year = themis.get_year(2023, 2024)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
#### `getCourse(courseName)`
|
#### `get_course(name)`
|
||||||
Returns an instance of a [`Course`](#course) with the name `courseName`.
|
Returns an instance of a [`Course`](#course) with the name `name`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
pf = year.get_course("Programming Fundamentals (for CS)")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `allCourses()`
|
#### `all_courses()`
|
||||||
Returns a list of `Course` instances corresponding to all courses visible to the user in a given `Year`.
|
Returns a list of `Course` instances corresponding to all courses visible to the user in a given `Year`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
courses = year.allCourses()
|
courses = year.all_courses()
|
||||||
```
|
```
|
||||||
|
|
||||||
----
|
----
|
||||||
@ -59,13 +59,13 @@ courses = year.allCourses()
|
|||||||
### Usage
|
### Usage
|
||||||
```python
|
```python
|
||||||
|
|
||||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
pf = year.get_course("Programming Fundamentals (for CS)")
|
||||||
print(pf.info) # <- course info attribute
|
print(pf.info) # <- course info attribute
|
||||||
assignments = pf.getGroups()
|
assignments = pf.get_groups()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
#### `getGroups(full=False)`
|
#### `get_groups(full=False)`
|
||||||
Returns a list of `ExerciseGroup` instances corresponding to all exercise groups visible to the user in a given `Course`. Default argument is `full=False`, which will only return the (name, link) of each exercise and folder in the group. If `full=True`, it will traverse the whole course.
|
Returns a list of `ExerciseGroup` instances corresponding to all exercise groups visible to the user in a given `Course`. Default argument is `full=False`, which will only return the (name, link) of each exercise and folder in the group. If `full=True`, it will traverse the whole course.
|
||||||
|
|
||||||
You can traverse the course in both cases, although in different ways.
|
You can traverse the course in both cases, although in different ways.
|
||||||
@ -73,24 +73,24 @@ You can traverse the course in both cases, although in different ways.
|
|||||||
When you have fully traversed the course, you can access everything via indices and the `exercises` and `folders` attributes of the `ExerciseGroup` instances:
|
When you have fully traversed the course, you can access everything via indices and the `exercises` and `folders` attributes of the `ExerciseGroup` instances:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ai_group = ai_course.getGroups(full=True)
|
ai_group = ai_course.get_groups(full=True)
|
||||||
exercise = ai_group[7].exercises[1] # Week 11 -> Suitcase packing
|
exercise = ai_group[7].exercises[1] # Week 11 -> Suitcase packing
|
||||||
exercise.submit("suitcase.py", silent=False)```
|
exercise.submit("suitcase.py", silent=False)```
|
||||||
```
|
```
|
||||||
|
|
||||||
This is equivalent to the case in which we don't traverse the full course using `getGroup` like so:
|
This is equivalent to the case in which we don't traverse the full course using `get_group` like so:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ai_group = ai_course.getGroup("Week 11")
|
ai_group = ai_course.get_group("Week 11")
|
||||||
exercise = ai_group.getGroup("Suitcase packing")
|
exercise = ai_group.get_group("Suitcase packing")
|
||||||
exercise.submit("suitcase.py", silent=False)
|
exercise.submit("suitcase.py", silent=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
### `getGroup(name, full=False)`
|
### `get_group(name, full=False)`
|
||||||
Returns an instance of an `ExerciseGroup` with the name `name`. Default argument is `full=False`, which will only return the (name, link) of each exercise and folder in the group. If `full=True`, it will traverse the whole group.
|
Returns an instance of an `ExerciseGroup` with the name `name`. Default argument is `full=False`, which will only return the (name, link) of each exercise and folder in the group. If `full=True`, it will traverse the whole group.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
week1 = pf.getGroup("Week 1")
|
week1 = pf.get_group("Week 1")
|
||||||
```
|
```
|
||||||
|
|
||||||
## `ExerciseGroup`
|
## `ExerciseGroup`
|
||||||
@ -98,9 +98,9 @@ Setting the `full` flag to `True` will traverse the whole course.
|
|||||||
|
|
||||||
You can traverse the course in both cases
|
You can traverse the course in both cases
|
||||||
* Both folders and exercises are represented as `ExerciseGroup` instances.
|
* Both folders and exercises are represented as `ExerciseGroup` instances.
|
||||||
* Folders will have the `amExercise` attribute set to `False`.
|
* Folders will have the `am_exercise` attribute set to `False`.
|
||||||
* Folders can have the `downloadFiles` method called on them.
|
* Folders can have the `download_files` method called on them.
|
||||||
* Exercises can have the `submit`, `downloadFiles` and `downloadTCs` method called on them.
|
* Exercises can have the `submit`, `download_files` and `download_tcs` method called on them.
|
||||||
|
|
||||||
|
|
||||||
### Example of folder traversal
|
### Example of folder traversal
|
||||||
@ -119,45 +119,45 @@ Let's say we have a folder structure like this:
|
|||||||
And we want to get to `Part 2` of `Week 1`'s `Exercise 2`. We would do this:
|
And we want to get to `Part 2` of `Week 1`'s `Exercise 2`. We would do this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
pf = year.get_course("Programming Fundamentals (for CS)")
|
||||||
assignments = pf.getExerciseGroups()
|
assignments = pf.get_groups()
|
||||||
week1 = assignments[0] # Week 1
|
week1 = assignments[0] # Week 1
|
||||||
exercise2 = week1.folders[1] # Exercise 2
|
exercise2 = week1.folders[1] # Exercise 2
|
||||||
part2 = exercise2.exercises[1] # Part 2
|
part2 = exercise2.exercises[1] # Part 2
|
||||||
|
|
||||||
# Or, if you dont want to traverse the whole course:
|
# Or, if you dont want to traverse the whole course:
|
||||||
week1 = pf.getGroup("Week 1")
|
week1 = pf.get_group("Week 1")
|
||||||
exercise2 = week1.getGroup("Exercise 2")
|
exercise2 = week1.get_group("Exercise 2")
|
||||||
part2 = exercise2.getGroup("Part 2")
|
part2 = exercise2.get_group("Part 2")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
#### `downloadFiles(path=".")`
|
#### `download_files(path=".")`
|
||||||
Downloads all files in the exercise group to a directory `path`. Defaults to the current directory.
|
Downloads all files in the exercise group to a directory `path`. Defaults to the current directory.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
assignment.downloadFiles()
|
assignment.download_files()
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `downloadTCs(path=".")`
|
#### `download_tcs(path=".")`
|
||||||
Downloads all test cases in the exercise group to a directory `path`. Defaults to the current directory.
|
Downloads all test cases in the exercise group to a directory `path`. Defaults to the current directory.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
assignment.downloadTCs()
|
assignment.download_tcs()
|
||||||
```
|
```
|
||||||
|
|
||||||
#### getGroup(name, full=False)
|
#### get_group(name, full=False)
|
||||||
This is used when you want to traverse the course dynamically(not recurse through the whole thing). Of course, you can use it even if you've traversed the whole course, but that would overcomplicate things.
|
This is used when you want to traverse the course dynamically(not recurse through the whole thing). Of course, you can use it even if you've traversed the whole course, but that would overcomplicate things.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Week 1 -> Exercise 2 -> Part 2
|
# Week 1 -> Exercise 2 -> Part 2
|
||||||
week1 = pf.getGroups("Week 1")
|
week1 = pf.get_groups("Week 1")
|
||||||
exercise2 = week1.getGroup("Exercise 2")
|
exercise2 = week1.get_group("Exercise 2")
|
||||||
part2 = exercise2.getGroup("Part 2")
|
part2 = exercise2.get_group("Part 2")
|
||||||
|
|
||||||
# This is equivalent to(but faster than):
|
# This is equivalent to(but faster than):
|
||||||
week1 = pf.getGroups("Week 1", full=True)
|
week1 = pf.get_groups("Week 1", full=True)
|
||||||
exercise2 = week1[1]
|
exercise2 = week1[1]
|
||||||
part2 = exercise2[1]
|
part2 = exercise2[1]
|
||||||
```
|
```
|
||||||
@ -167,8 +167,12 @@ This is used when you want to traverse the course dynamically(not recurse throug
|
|||||||
Submits the files to the exercise group. Default arguments are `judge=True`, `wait=True` and `silent=True`. `judge` will judge the submission instantly, and `wait` will wait for the submission to finish. Turning off `silent` will print the submission status dynamically.
|
Submits the files to the exercise group. Default arguments are `judge=True`, `wait=True` and `silent=True`. `judge` will judge the submission instantly, and `wait` will wait for the submission to finish. Turning off `silent` will print the submission status dynamically.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
suitcase = ai.get_group("Week 11")
|
||||||
suitcase[7].exercises[1].submit("suitcase.py", silent=False)
|
suitcase[7].exercises[1].submit("suitcase.py", silent=False)
|
||||||
|
|
||||||
|
# Or
|
||||||
|
ai.get_group("Week 11").get_group("Suitcase packing").submit("suitcase.py", silent=False)
|
||||||
|
|
||||||
>>> 1: ✅
|
>>> 1: ✅
|
||||||
>>> 2: ✅
|
>>> 2: ✅
|
||||||
>>> 3: ✅
|
>>> 3: ✅
|
||||||
@ -179,6 +183,7 @@ Submits the files to the exercise group. Default arguments are `judge=True`, `wa
|
|||||||
>>> 8: ✅
|
>>> 8: ✅
|
||||||
>>> 9: ✅
|
>>> 9: ✅
|
||||||
>>> 10: ✅
|
>>> 10: ✅
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,22 +18,22 @@ pip install temmies
|
|||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
```python
|
```python
|
||||||
import temmies
|
from temmies.themis import Themis
|
||||||
|
|
||||||
# Log in
|
# Log in
|
||||||
themis = temmies.Themis("s-number", "password")
|
themis = Themis("s-number", "password")
|
||||||
|
|
||||||
# Get a year
|
# Get a year
|
||||||
year = themis.getYear(2023, 2024)
|
year = themis.get_year(2023, 2024)
|
||||||
|
|
||||||
# Get a course
|
# Get a course
|
||||||
pf = year.getCourse("Programming Fundamentals (for CS)")
|
course = year.get_course("Programming Fundamentals (for CS)")
|
||||||
|
|
||||||
# Get an assignment
|
# Get an assignment
|
||||||
pf_assignment = pf.getGroup("Assignment 1")
|
assignment = course.get_assignment("Assignment 1")
|
||||||
|
|
||||||
# Get a specific exercise
|
# Submit 2 files
|
||||||
exercise = pf_assignment.getGroup("Exercise 1")
|
assignment.submit(["among.c", "us.py"])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 141 KiB |
@ -1,48 +0,0 @@
|
|||||||
# Class to handle courses
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from requests import Session
|
|
||||||
from ExerciseGroup import ExerciseGroup
|
|
||||||
import re
|
|
||||||
from exceptions.CourseUnavailable import CourseUnavailable
|
|
||||||
|
|
||||||
class Course:
|
|
||||||
# Extend the Base class init
|
|
||||||
def __init__(self, url:str, name:str, session:Session, parent):
|
|
||||||
self.url = url
|
|
||||||
self.name = name
|
|
||||||
self.__session = session
|
|
||||||
self.__parent = parent
|
|
||||||
self.__request = self.__session.get(self.url)
|
|
||||||
self.__raw = BeautifulSoup(self.__request.text, 'lxml')
|
|
||||||
|
|
||||||
self.__courseAvailable(self.__session.get(self.url))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Course {self.name} in year {self.__parent.year}"
|
|
||||||
|
|
||||||
def __courseAvailable(self, r):
|
|
||||||
# Check if we got an error
|
|
||||||
# print(self.url)
|
|
||||||
if "Something went wrong" in r.text:
|
|
||||||
raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ")
|
|
||||||
|
|
||||||
def getGroups(self, full:bool=False):
|
|
||||||
section = self.__raw.find('div', class_="ass-children")
|
|
||||||
entries = section.find_all('a', href=True)
|
|
||||||
return [
|
|
||||||
ExerciseGroup(
|
|
||||||
f"https://themis.housing.rug.nl{x['href']}",
|
|
||||||
x,
|
|
||||||
self.__session,
|
|
||||||
self,
|
|
||||||
full
|
|
||||||
)
|
|
||||||
for x in entries]
|
|
||||||
|
|
||||||
# BAD: Repeated code!!!!
|
|
||||||
def getGroup(self, name:str, full:bool=False):
|
|
||||||
group = self.__raw.find("a", text=name)
|
|
||||||
if not group:
|
|
||||||
raise IllegalAction(message=f"No such group found: {name}")
|
|
||||||
|
|
||||||
return ExerciseGroup(f"https://themis.housing.rug.nl{group['href']}", group, self.__session, self, full)
|
|
@ -1,231 +0,0 @@
|
|||||||
from bs4 import BeautifulSoup
|
|
||||||
from exceptions.IllegalAction import IllegalAction
|
|
||||||
import re
|
|
||||||
from json import loads
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
class ExerciseGroup:
|
|
||||||
def __init__(self, url: str, soup, session, parent, full:bool):
|
|
||||||
self.url = url
|
|
||||||
self.name = soup.text
|
|
||||||
self.__prev_raw = soup
|
|
||||||
self.__session = session
|
|
||||||
self.__request = self.__session.get(self.url)
|
|
||||||
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
|
||||||
self.__full = full
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def amExercise(self):
|
|
||||||
return "ass-submitable" in self.__prev_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.__raw.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.__raw.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
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exercises(self) -> list:
|
|
||||||
if self.amExercise:
|
|
||||||
return self
|
|
||||||
|
|
||||||
section = self.__raw.find("div", class_="ass-children")
|
|
||||||
try:
|
|
||||||
submittables = section.find_all("a", class_="ass-submitable")
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not self.__full:
|
|
||||||
return [(x.text,x['href']) for x in submittables]
|
|
||||||
return [
|
|
||||||
ExerciseGroup(
|
|
||||||
f"https://themis.housing.rug.nl{x['href']}", x, self.__session, self, True
|
|
||||||
)
|
|
||||||
for x in submittables
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def folders(self) -> list:
|
|
||||||
section = self.__raw.find("div", class_="ass-children")
|
|
||||||
try:
|
|
||||||
folders = section.find_all("a", class_="ass-group")
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not self.__full:
|
|
||||||
return [(x.text,x['href']) for x in folders]
|
|
||||||
|
|
||||||
return [
|
|
||||||
ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", x, self.__session, self, True)
|
|
||||||
for x in folders
|
|
||||||
]
|
|
||||||
|
|
||||||
# Get by name
|
|
||||||
def getGroup(self, name:str, full:bool=False, link:str=None):
|
|
||||||
if link:
|
|
||||||
return ExerciseGroup(link, self.__prev_raw, self.__session, self, full)
|
|
||||||
|
|
||||||
group = self.__raw.find("a", text=name)
|
|
||||||
if not group:
|
|
||||||
raise IllegalAction(message=f"No such group found: {name}")
|
|
||||||
|
|
||||||
return ExerciseGroup(f"https://themis.housing.rug.nl{group['href']}", group, self.__session, self, full)
|
|
||||||
|
|
||||||
# Account for judge
|
|
||||||
def __raceCondition(self, soup, url:str, verbose:bool):
|
|
||||||
self.__session.get(url.replace("submission", "judge"))
|
|
||||||
return self.__waitForResult(url, verbose, [])
|
|
||||||
|
|
||||||
def __parseTable(self, soup, url:str, verbose:bool, __printed:list):
|
|
||||||
cases = soup.find_all('tr', class_='sub-casetop')
|
|
||||||
fail_pass = {}
|
|
||||||
i = 1
|
|
||||||
for case in cases:
|
|
||||||
name = case.find('td', class_='sub-casename').text
|
|
||||||
status = case.find('td', class_='status-icon')
|
|
||||||
|
|
||||||
if "pending" in status.get("class"):
|
|
||||||
return self.__raceCondition(soup,url,verbose)
|
|
||||||
|
|
||||||
# queued status-icon
|
|
||||||
if "queued" in status.get("class"):
|
|
||||||
sleep(1) # <- 🗿
|
|
||||||
return self.__waitForResult(url, verbose, __printed)
|
|
||||||
|
|
||||||
if "Passed" in status.text:
|
|
||||||
fail_pass[int(name)] = True
|
|
||||||
if int(name) not in __printed and verbose == True:
|
|
||||||
print(f"{name}: ✅")
|
|
||||||
elif "Wrong output" in status.text:
|
|
||||||
fail_pass[int(name)] = False
|
|
||||||
if int(name) not in __printed and verbose == True:
|
|
||||||
print(f"{name}: ❌")
|
|
||||||
elif ("No status" or "error") in status.text:
|
|
||||||
fail_pass[int(name)] = None
|
|
||||||
if int(name) not in __printed and verbose == True:
|
|
||||||
print(f"{name}:🐛")
|
|
||||||
|
|
||||||
__printed.append(int(name))
|
|
||||||
i += 1
|
|
||||||
return fail_pass
|
|
||||||
|
|
||||||
def __waitForResult(self, url:str, verbose:bool, __printed:list):
|
|
||||||
# This waits for result and returns a bundled info package
|
|
||||||
r = self.__session.get(url)
|
|
||||||
soup = BeautifulSoup(r.text, "lxml")
|
|
||||||
return self.__parseTable(soup, url, verbose, __printed)
|
|
||||||
|
|
||||||
|
|
||||||
# Submit
|
|
||||||
def submit(self, files: list, judge=True, wait=True, silent=True):
|
|
||||||
|
|
||||||
# Find the form with submit and store the action as url
|
|
||||||
# Store then the data-suffixes as file_types - dictionary
|
|
||||||
|
|
||||||
form = self.__raw.find("form")
|
|
||||||
if not form:
|
|
||||||
raise IllegalAction(message="You cannot submit to this assignment.")
|
|
||||||
|
|
||||||
url = "https://themis.housing.rug.nl" + form["action"]
|
|
||||||
file_types = loads(form["data-suffixes"])
|
|
||||||
|
|
||||||
if isinstance(files, str):
|
|
||||||
temp = []
|
|
||||||
temp.append(files)
|
|
||||||
files = temp
|
|
||||||
|
|
||||||
# Package the files up into files[]
|
|
||||||
# DEBUG: Uncomment for better clarity
|
|
||||||
# print("Submitting files:")
|
|
||||||
# [print(f) for f in files]
|
|
||||||
packaged_files = []
|
|
||||||
data = {}
|
|
||||||
found_type = ""
|
|
||||||
for file in files:
|
|
||||||
for t in file_types:
|
|
||||||
if t in file:
|
|
||||||
found_type = file_types[t]
|
|
||||||
break
|
|
||||||
if not found_type:
|
|
||||||
raise IllegalAction(message="Illegal filetype for this assignment.")
|
|
||||||
|
|
||||||
packaged_files.append((f"files[]", (file, open(file, "rb"), "text/x-csrc")))
|
|
||||||
|
|
||||||
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type}
|
|
||||||
|
|
||||||
resp = self.__session.post(url, files=packaged_files, data=data)
|
|
||||||
|
|
||||||
# Close each file
|
|
||||||
i = 0
|
|
||||||
for f in packaged_files:
|
|
||||||
f[1][1].close()
|
|
||||||
|
|
||||||
if not wait:
|
|
||||||
return resp.url if "@submissions" in resp.url else None
|
|
||||||
|
|
||||||
return self.__waitForResult(resp.url, not silent, [])
|
|
@ -1,62 +0,0 @@
|
|||||||
from Year import Year
|
|
||||||
import urllib3
|
|
||||||
from requests import Session
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
# Disable warnings
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
|
|
||||||
class Themis:
|
|
||||||
def __init__(self,user:str, passwd:str):
|
|
||||||
self.session = self.login(user,passwd)
|
|
||||||
self.years = []
|
|
||||||
self.url = "https://themis.housing.rug.nl/course/"
|
|
||||||
|
|
||||||
def login(self, user, passwd):
|
|
||||||
headers = {
|
|
||||||
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chromium/80.0.3987.160 Chrome/80.0.3987.163 Safari/537.36"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"user": user,
|
|
||||||
"password":passwd,
|
|
||||||
"null": None
|
|
||||||
}
|
|
||||||
|
|
||||||
with Session() as s:
|
|
||||||
url = 'https://themis.housing.rug.nl/log/in'
|
|
||||||
r = s.get(url,headers=headers,verify=False)
|
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
|
|
||||||
# get the csrf token and add it to payload
|
|
||||||
csrfToken = soup.find('input',attrs = {'name':'_csrf'})['value']
|
|
||||||
data['_csrf'] = csrfToken
|
|
||||||
data['sudo'] = user.lower()
|
|
||||||
|
|
||||||
# Login
|
|
||||||
r = s.post(url,data=data,headers = headers)
|
|
||||||
|
|
||||||
# check if login was successful
|
|
||||||
log_out = "Welcome, logged in as" in r.text
|
|
||||||
if not log_out:
|
|
||||||
raise Exception(f"Login for user {user} failed")
|
|
||||||
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def getYear(self, start:int, end:int):
|
|
||||||
return Year(self.session, self, start, end)
|
|
||||||
|
|
||||||
def allYears(self):
|
|
||||||
# All of them are in a big ul at the beginning of the page
|
|
||||||
r = self.session.get(self.url)
|
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
ul = soup.find('ul', class_='round')
|
|
||||||
lis = ul.find_all('li', class_='large')
|
|
||||||
years = []
|
|
||||||
for li in lis:
|
|
||||||
# format: 2019-2020
|
|
||||||
year = li.a.text.split("-")
|
|
||||||
years.append(Year(self.session, self, int(year[0]), int(year[1])))
|
|
||||||
|
|
||||||
return years # Return a list of year objects
|
|
55
src/Year.py
55
src/Year.py
@ -1,55 +0,0 @@
|
|||||||
# Year class to represent an academic year
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from Course import Course
|
|
||||||
from requests import Session
|
|
||||||
from exceptions.CourseUnavailable import CourseUnavailable
|
|
||||||
|
|
||||||
# Works
|
|
||||||
class Year:
|
|
||||||
def __init__(self, session:Session, parent, start_year:int, end_year:int):
|
|
||||||
self.start = start_year
|
|
||||||
self.year = end_year
|
|
||||||
self.url = self.__constructUrl()
|
|
||||||
self.__session = session
|
|
||||||
|
|
||||||
# Method to set the url
|
|
||||||
def __constructUrl(self):
|
|
||||||
return f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
|
||||||
|
|
||||||
# Method to get the courses of the year
|
|
||||||
def allCourses(self, errors:bool=False) -> list[Course]:
|
|
||||||
# lis in a big ul
|
|
||||||
r = self.__session.get(self.url)
|
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
lis = soup.find_all('li', class_='large')
|
|
||||||
courses = []
|
|
||||||
for li in lis:
|
|
||||||
try:
|
|
||||||
suffix = (li.a['href'].replace(f"course/{self.start}-{self.year}", ""))
|
|
||||||
courses.append(
|
|
||||||
Course(
|
|
||||||
self.url + suffix,
|
|
||||||
li.a.text,
|
|
||||||
self.__session,
|
|
||||||
self
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except CourseUnavailable:
|
|
||||||
if errors:
|
|
||||||
raise CourseUnavailable(f"Course {li.a.text} in year {self.start}-{self.year} is not available")
|
|
||||||
else:
|
|
||||||
print("error with course", li.a.text)
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
return courses
|
|
||||||
|
|
||||||
def getCourse(self, name:str) -> Course:
|
|
||||||
# Get the course
|
|
||||||
r = self.__session.get(self.url)
|
|
||||||
soup = BeautifulSoup(r.text, 'lxml')
|
|
||||||
# Search by name
|
|
||||||
course = self.url + soup.find('a', text=name)['href'].replace(f"course/{self.start}-{self.year}", "")
|
|
||||||
# Get the url and transform it into a course object
|
|
||||||
return Course(url=course, name=name, session=self.__session, parent=self)
|
|
71
src/course.py
Normal file
71
src/course.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Houses the Course class which is used to represent a course in a year.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from requests import Session
|
||||||
|
from exercise_group import ExerciseGroup
|
||||||
|
from exceptions.course_unavailable import CourseUnavailable
|
||||||
|
from exceptions.illegal_action import IllegalAction
|
||||||
|
|
||||||
|
|
||||||
|
class Course:
|
||||||
|
"""
|
||||||
|
get_groups: Get all groups in a course. Set full to True to get all subgroups.
|
||||||
|
get_group: Get a group by name. Set full to True to get all subgroups.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url: str, name: str, session: Session, parent):
|
||||||
|
self.url = url
|
||||||
|
self.name = name
|
||||||
|
self.__session = session
|
||||||
|
self.__parent = parent
|
||||||
|
self.__request = self.__session.get(self.url)
|
||||||
|
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
||||||
|
|
||||||
|
self.__course_available(self.__session.get(self.url))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Course {self.name} in year {self.__parent.year}"
|
||||||
|
|
||||||
|
def __course_available(self, r):
|
||||||
|
# Check if we got an error
|
||||||
|
# print(self.url)
|
||||||
|
if "Something went wrong" in r.text:
|
||||||
|
raise CourseUnavailable(
|
||||||
|
message="'Something went wrong'. Course most likely not found. "
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_groups(self, full: bool = False) -> list[ExerciseGroup]:
|
||||||
|
"""
|
||||||
|
get_groups(full: bool = False) -> list[ExerciseGroup]
|
||||||
|
Get all groups in a course. Set full to True to get all subgroups.
|
||||||
|
"""
|
||||||
|
section = self.__raw.find("div", class_="ass-children")
|
||||||
|
entries = section.find_all("a", href=True)
|
||||||
|
return [
|
||||||
|
ExerciseGroup(
|
||||||
|
f"https://themis.housing.rug.nl{x['href']}",
|
||||||
|
x,
|
||||||
|
self.__session,
|
||||||
|
full
|
||||||
|
)
|
||||||
|
for x in entries
|
||||||
|
]
|
||||||
|
|
||||||
|
# BAD: Repeated code!!!!
|
||||||
|
def get_group(self, name: str, full: bool = False) -> ExerciseGroup:
|
||||||
|
"""
|
||||||
|
get_group(name:str, full:bool = False) -> ExerciseGroup
|
||||||
|
Get a single group by name. Set full to True to get all subgroups as well.
|
||||||
|
"""
|
||||||
|
group = self.__raw.find("a", text=name)
|
||||||
|
if not group:
|
||||||
|
raise IllegalAction(message=f"No such group found: {name}")
|
||||||
|
|
||||||
|
return ExerciseGroup(
|
||||||
|
f"https://themis.housing.rug.nl{group['href']}",
|
||||||
|
group,
|
||||||
|
self.__session,
|
||||||
|
full
|
||||||
|
)
|
@ -1,4 +0,0 @@
|
|||||||
class CourseUnavailable(Exception):
|
|
||||||
def __init__(self, message:str=""):
|
|
||||||
self.message = "Course Error: " + message
|
|
||||||
super().__init__(self.message)
|
|
@ -1,4 +0,0 @@
|
|||||||
class IllegalAction(Exception):
|
|
||||||
def __init__(self, message:str=""):
|
|
||||||
self.message = "Illegal action: " + message
|
|
||||||
super().__init__(self.message)
|
|
6
src/exceptions/course_unavailable.py
Normal file
6
src/exceptions/course_unavailable.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
""" This module contains the CourseUnavailable exception. """
|
||||||
|
|
||||||
|
class CourseUnavailable(Exception):
|
||||||
|
"""CourseUnavailable Exception"""
|
||||||
|
def __init__(self, message: str = ""):
|
||||||
|
super().__init__(f"Course unavailable: {message}")
|
8
src/exceptions/illegal_action.py
Normal file
8
src/exceptions/illegal_action.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Illegal Action Exception
|
||||||
|
"""
|
||||||
|
|
||||||
|
class IllegalAction(Exception):
|
||||||
|
"""Illegal Action Exception"""
|
||||||
|
def __init__(self, message: str = ""):
|
||||||
|
super().__init__(f"Illegal action: {message}")
|
270
src/exercise_group.py
Normal file
270
src/exercise_group.py
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
"""
|
||||||
|
Houses the ExerciseGroup class.
|
||||||
|
Represents a group of exercises or a single exercise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from json import loads
|
||||||
|
from time import sleep
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from exceptions.illegal_action import IllegalAction
|
||||||
|
|
||||||
|
|
||||||
|
class ExerciseGroup:
|
||||||
|
"""
|
||||||
|
am_exercise: returns bool which tells you if the instance is an exercise
|
||||||
|
submit: submit to an exercise
|
||||||
|
get_group: get a group by name
|
||||||
|
get_groups: get all groups
|
||||||
|
folders: folders in the folder
|
||||||
|
exercises: exercises in the folder
|
||||||
|
test_cases: test cases in the exercise(if it is an exercise)
|
||||||
|
download_tcs: download test cases
|
||||||
|
files: files in the exercise/folder
|
||||||
|
download_files: download files
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url: str, soup, session, full: bool):
|
||||||
|
self.url = url
|
||||||
|
self.name = soup.text
|
||||||
|
self.__prev_raw = soup
|
||||||
|
self.__session = session
|
||||||
|
self.__request = self.__session.get(self.url)
|
||||||
|
self.__raw = BeautifulSoup(self.__request.text, "lxml")
|
||||||
|
self.__full = full
|
||||||
|
|
||||||
|
@property
|
||||||
|
def am_exercise(self) -> bool:
|
||||||
|
return "ass-submitable" in self.__prev_raw["class"]
|
||||||
|
|
||||||
|
# Test cases
|
||||||
|
@property
|
||||||
|
def test_cases(self) -> list[str]:
|
||||||
|
section = self.__raw.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
|
||||||
|
|
||||||
|
def download_tcs(self, path=".") -> list[str]:
|
||||||
|
"""
|
||||||
|
download_tcs(path=".") -> list[str]
|
||||||
|
Downloads every test case available from a given exercise. `path` defaults to '.'.
|
||||||
|
"""
|
||||||
|
if not self.am_exercise:
|
||||||
|
raise IllegalAction(message="You are downloading test cases from a folder.")
|
||||||
|
|
||||||
|
for tc in self.test_cases:
|
||||||
|
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.test_cases
|
||||||
|
|
||||||
|
# Files
|
||||||
|
@property
|
||||||
|
def files(self) -> list[str]:
|
||||||
|
details = self.__raw.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 i in a:
|
||||||
|
link_list.append(i)
|
||||||
|
|
||||||
|
return link_list
|
||||||
|
|
||||||
|
def download_files(self, path=".") -> list[str]:
|
||||||
|
"""
|
||||||
|
download_files(path=".") -> list[str]
|
||||||
|
Downloads every file available from a given exercise/folder. `path` defaults to '.'.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exercises(self) -> list[str] | list["ExerciseGroup"]:
|
||||||
|
if self.am_exercise:
|
||||||
|
return self
|
||||||
|
|
||||||
|
section = self.__raw.find("div", class_="ass-children")
|
||||||
|
try:
|
||||||
|
submittables = section.find_all("a", class_="ass-submitable")
|
||||||
|
except AttributeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not self.__full:
|
||||||
|
return [(x.text, x["href"]) for x in submittables]
|
||||||
|
return [
|
||||||
|
ExerciseGroup(
|
||||||
|
f"https://themis.housing.rug.nl{x['href']}", x, self.__session, True
|
||||||
|
)
|
||||||
|
for x in submittables
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folders(self) -> list[str] | list["ExerciseGroup"]:
|
||||||
|
section = self.__raw.find("div", class_="ass-children")
|
||||||
|
try:
|
||||||
|
folders = section.find_all("a", class_="ass-group")
|
||||||
|
except AttributeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not self.__full:
|
||||||
|
return [(x.text, x["href"]) for x in folders]
|
||||||
|
|
||||||
|
return [
|
||||||
|
ExerciseGroup(
|
||||||
|
f"https://themis.housing.rug.nl{x['href']}", x, self.__session, True
|
||||||
|
)
|
||||||
|
for x in folders
|
||||||
|
]
|
||||||
|
|
||||||
|
# Get by name
|
||||||
|
def get_group(
|
||||||
|
self, name: str, full: bool = False, link: str = None
|
||||||
|
) -> "ExerciseGroup":
|
||||||
|
"""
|
||||||
|
get_group(name:str, full:bool=False, link:str=None) -> ExerciseGroup | list[ExerciseGroup]
|
||||||
|
Get a single group by name.
|
||||||
|
Set `full` to True to get all subgroups as well.
|
||||||
|
Set `link` to directly fetch a group.
|
||||||
|
"""
|
||||||
|
if link:
|
||||||
|
return ExerciseGroup(link, self.__prev_raw, self.__session, full)
|
||||||
|
|
||||||
|
group = self.__raw.find("a", text=name)
|
||||||
|
if not group:
|
||||||
|
raise IllegalAction(message=f"No such group found: {name}")
|
||||||
|
|
||||||
|
return ExerciseGroup(
|
||||||
|
f"https://themis.housing.rug.nl{group['href']}", group, self.__session, full
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait for result
|
||||||
|
def __wait_for_result(self, url: str, verbose: bool, __printed: list) -> None:
|
||||||
|
# This waits for result and returns a bundled info package
|
||||||
|
r = self.__session.get(url)
|
||||||
|
soup = BeautifulSoup(r.text, "lxml")
|
||||||
|
return self.__parse_table(soup, url, verbose, __printed)
|
||||||
|
|
||||||
|
# Account for judge
|
||||||
|
def __race_condition(self, url: str, verbose: bool) -> None:
|
||||||
|
self.__session.get(url.replace("submission", "judge"))
|
||||||
|
return self.__wait_for_result(url, verbose, [])
|
||||||
|
|
||||||
|
def __parse_table(
|
||||||
|
self, soup: BeautifulSoup, url: str, verbose: bool, __printed: list
|
||||||
|
) -> dict:
|
||||||
|
cases = soup.find_all("tr", class_="sub-casetop")
|
||||||
|
fail_pass = {}
|
||||||
|
i = 1
|
||||||
|
for case in cases:
|
||||||
|
name = case.find("td", class_="sub-casename").text
|
||||||
|
status = case.find("td", class_="status-icon")
|
||||||
|
|
||||||
|
if "pending" in status.get("class"):
|
||||||
|
return self.__race_condition(url, verbose)
|
||||||
|
|
||||||
|
# queued status-icon
|
||||||
|
if "queued" in status.get("class"):
|
||||||
|
sleep(1) # <- 🗿
|
||||||
|
return self.__wait_for_result(url, verbose, __printed)
|
||||||
|
|
||||||
|
statuses = {
|
||||||
|
"Passed": ("✅", True),
|
||||||
|
"Wrong output": ("❌", False),
|
||||||
|
"No status": ("🐛", None),
|
||||||
|
"error": ("🐛", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Printing and storing
|
||||||
|
found = False
|
||||||
|
for k, v in statuses.items():
|
||||||
|
if k in status.text:
|
||||||
|
found = True
|
||||||
|
if verbose and int(name) not in __printed:
|
||||||
|
print(f"{name}: {v[0]}")
|
||||||
|
fail_pass[int(name)] = v[1]
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
fail_pass[int(name)] = None
|
||||||
|
if verbose and int(name) not in __printed:
|
||||||
|
print(f"{name}: Unrecognized status: {status.text}")
|
||||||
|
|
||||||
|
__printed.append(int(name))
|
||||||
|
i += 1
|
||||||
|
return fail_pass
|
||||||
|
|
||||||
|
# Submit
|
||||||
|
def submit(
|
||||||
|
self, files: list, judge: bool = True, wait: bool = True, silent: bool = True
|
||||||
|
) -> dict | None:
|
||||||
|
"""
|
||||||
|
submit(files:list, judge:bool=True, wait:bool=True, silent:bool=True) -> dict | None
|
||||||
|
Submits given files to given exercise. Returns a dictionary of test cases and their status.
|
||||||
|
Set judge to False to not judge the submission.
|
||||||
|
Set wait to False to not wait for the result.
|
||||||
|
Set silent to False to print the results.
|
||||||
|
"""
|
||||||
|
form = self.__raw.find("form")
|
||||||
|
if not form:
|
||||||
|
raise IllegalAction(message="You cannot submit to this assignment.")
|
||||||
|
|
||||||
|
url = "https://themis.housing.rug.nl" + form["action"]
|
||||||
|
file_types = loads(form["data-suffixes"])
|
||||||
|
|
||||||
|
if isinstance(files, str):
|
||||||
|
temp = []
|
||||||
|
temp.append(files)
|
||||||
|
files = temp
|
||||||
|
|
||||||
|
packaged_files = []
|
||||||
|
data = {}
|
||||||
|
found_type = ""
|
||||||
|
for file in files:
|
||||||
|
for t in file_types:
|
||||||
|
if t in file:
|
||||||
|
found_type = file_types[t]
|
||||||
|
break
|
||||||
|
if not found_type:
|
||||||
|
raise IllegalAction(message="Illegal filetype for this assignment.")
|
||||||
|
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
packaged_files.append((found_type, (file, f.read())))
|
||||||
|
|
||||||
|
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type}
|
||||||
|
|
||||||
|
if not silent:
|
||||||
|
print(f"Submitting to {self.name}")
|
||||||
|
for file in files:
|
||||||
|
print(f"• {file}")
|
||||||
|
resp = self.__session.post(url, files=packaged_files, data=data)
|
||||||
|
|
||||||
|
if not wait or not judge:
|
||||||
|
return resp.url if "@submissions" in resp.url else None
|
||||||
|
|
||||||
|
return self.__wait_for_result(resp.url, not silent, [])
|
90
src/themis.py
Normal file
90
src/themis.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""
|
||||||
|
Main class for the Themis API
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import urllib3
|
||||||
|
from requests import Session
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from year import Year
|
||||||
|
from exceptions.illegal_action import IllegalAction
|
||||||
|
|
||||||
|
|
||||||
|
# Disable warnings
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class Themis:
|
||||||
|
"""
|
||||||
|
login: Login to Themis
|
||||||
|
get_year: Get a year object
|
||||||
|
all_years: Get all years
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, user: str, passwd: str):
|
||||||
|
self.session = self.login(user, passwd)
|
||||||
|
self.years = []
|
||||||
|
self.url = "https://themis.housing.rug.nl/course/"
|
||||||
|
|
||||||
|
def login(self, user: str, passwd: str) -> Session:
|
||||||
|
"""
|
||||||
|
login(self, user: str, passwd: str) -> Session
|
||||||
|
Login to Themis
|
||||||
|
Set user to your student number and passwd to your password
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_agent = (
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chromium/80.0.3987.160 Chrome/80.0.3987.163 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {"user-agent": user_agent}
|
||||||
|
|
||||||
|
data = {"user": user, "password": passwd, "null": None}
|
||||||
|
|
||||||
|
with Session() as s:
|
||||||
|
url = "https://themis.housing.rug.nl/log/in"
|
||||||
|
r = s.get(url, headers=headers, verify=False)
|
||||||
|
soup = BeautifulSoup(r.text, "lxml")
|
||||||
|
|
||||||
|
# get the csrf token and add it to payload
|
||||||
|
csrf_token = soup.find("input", attrs={"name": "_csrf"})["value"]
|
||||||
|
data["_csrf"] = csrf_token
|
||||||
|
data["sudo"] = user.lower()
|
||||||
|
|
||||||
|
# Login
|
||||||
|
r = s.post(url, data=data, headers=headers)
|
||||||
|
|
||||||
|
# check if login was successful
|
||||||
|
log_out = "Welcome, logged in as" in r.text
|
||||||
|
if not log_out:
|
||||||
|
raise IllegalAction(message=f"Login for user {user} failed")
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
def get_year(self, start: int, end: int) -> Year:
|
||||||
|
"""
|
||||||
|
get_year(self, start: int, end: int) -> Year
|
||||||
|
Gets a year object
|
||||||
|
Set start to the start year and end to the end year (e.g. 2023-2024)
|
||||||
|
"""
|
||||||
|
return Year(self.session, start, end)
|
||||||
|
|
||||||
|
def all_years(self) -> list[Year]:
|
||||||
|
"""
|
||||||
|
get_years(self, start: int, end: int) -> list[Year]
|
||||||
|
Gets all visible years
|
||||||
|
"""
|
||||||
|
# All of them are in a big ul at the beginning of the page
|
||||||
|
r = self.session.get(self.url)
|
||||||
|
soup = BeautifulSoup(r.text, "lxml")
|
||||||
|
ul = soup.find("ul", class_="round")
|
||||||
|
lis = ul.find_all("li", class_="large")
|
||||||
|
years = []
|
||||||
|
for li in lis:
|
||||||
|
# format: 2019-2020
|
||||||
|
year = li.a.text.split("-")
|
||||||
|
years.append(Year(self.session, int(year[0]), int(year[1])))
|
||||||
|
|
||||||
|
return years # Return a list of year objects
|
66
src/year.py
Normal file
66
src/year.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
Class which represents an academic year.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from course import Course
|
||||||
|
from exceptions.course_unavailable import CourseUnavailable
|
||||||
|
|
||||||
|
|
||||||
|
# Works
|
||||||
|
class Year:
|
||||||
|
"""
|
||||||
|
all_courses: Get all visible courses in a year
|
||||||
|
get_course: Get a course by name
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session: Session, start_year: int, end_year: int):
|
||||||
|
self.start = start_year
|
||||||
|
self.year = end_year
|
||||||
|
self.url = f"https://themis.housing.rug.nl/course/{self.start}-{self.year}"
|
||||||
|
self.__session = session
|
||||||
|
|
||||||
|
# Method to get the courses of the year
|
||||||
|
def all_courses(self, errors: bool = True) -> list[Course]:
|
||||||
|
"""
|
||||||
|
all_courses(self, errors: bool = False) -> list[Course]
|
||||||
|
Gets all visible courses in a year.
|
||||||
|
Set errors to False to not raise an error when a course is unavailable.
|
||||||
|
"""
|
||||||
|
r = self.__session.get(self.url)
|
||||||
|
soup = BeautifulSoup(r.text, "lxml")
|
||||||
|
lis = soup.find_all("li", class_="large")
|
||||||
|
courses = []
|
||||||
|
for li in lis:
|
||||||
|
try:
|
||||||
|
suffix = li.a["href"].replace(f"course/{self.start}-{self.year}", "")
|
||||||
|
courses.append(
|
||||||
|
Course(self.url + suffix, li.a.text, self.__session, self)
|
||||||
|
)
|
||||||
|
except CourseUnavailable as exc:
|
||||||
|
if errors:
|
||||||
|
raise CourseUnavailable(
|
||||||
|
message=f"Course {li.a.text} in year {self.start}-{self.year} unavailable"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
print("Error with course", li.a.text)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return courses
|
||||||
|
|
||||||
|
def get_course(self, name: str) -> Course:
|
||||||
|
"""
|
||||||
|
get_course(self, name: str) -> Course
|
||||||
|
Gets a course by name.
|
||||||
|
"""
|
||||||
|
# Get the course
|
||||||
|
r = self.__session.get(self.url)
|
||||||
|
soup = BeautifulSoup(r.text, "lxml")
|
||||||
|
# Search by name
|
||||||
|
course = self.url + soup.find("a", text=name)["href"].replace(
|
||||||
|
f"course/{self.start}-{self.year}", ""
|
||||||
|
)
|
||||||
|
# Get the url and transform it into a course object
|
||||||
|
return Course(url=course, name=name, session=self.__session, parent=self)
|
Loading…
x
Reference in New Issue
Block a user