Updated docs and included getGroup(by name). Privated some attributes that aren't necessary to be public.

This commit is contained in:
Boyan 2024-04-09 16:18:54 +02:00
parent 27d21ac7c1
commit 11864cae6b
9 changed files with 118 additions and 299 deletions

View File

@ -61,39 +61,48 @@ courses = year.allCourses()
pf = year.getCourse("Programming Fundamentals (for CS)") pf = year.getCourse("Programming Fundamentals (for CS)")
print(pf.info) # <- course info attribute print(pf.info) # <- course info attribute
assignments = pf.getExerciseGroups() assignments = pf.getGroups()
``` ```
### Methods ### Methods
#### `getExerciseGroups()` #### `getGroups(full=False)`
Returns a list of `ExerciseGroup` instances corresponding to all exercise groups visible to the user in a given `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.
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
assignments = pf.getExerciseGroups() ai_group = ai_course.getGroups(full=True)
exercise = ai_group[7].exercises[1] # Week 11 -> Suitcase packing
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:
```python
ai_group = ai_course.getGroup("Week 11")
exercise = ai_group.getGroup("Suitcase packing")
exercise.submit("suitcase.py", silent=False)
```
### `getGroup(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.
```python
week1 = pf.getGroup("Week 1")
``` ```
## `ExerciseGroup` ## `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). Setting the `full` flag to `True` will traverse the whole course.
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 `amExercise` attribute set to `False`.
* Folders can have the `downloadFiles` method called on them. * Folders can have the `downloadFiles` method called on them.
* Exercises can have the `submit`, `downloadFiles` and `downloadTCs` 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 ### Example of folder traversal
Let's say we have a folder structure like this: Let's say we have a folder structure like this:
``` ```
@ -112,9 +121,14 @@ 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.getCourse("Programming Fundamentals (for CS)")
assignments = pf.getExerciseGroups() assignments = pf.getExerciseGroups()
week1 = assignments[0].folders[0] week1 = assignments[0] # Week 1
exercise2 = week1.exercises[1] exercise2 = week1.folders[1] # Exercise 2
part2 = exercise2.folders[1] part2 = exercise2.exercises[1] # Part 2
# Or, if you dont want to traverse the whole course:
week1 = pf.getGroup("Week 1")
exercise2 = week1.getGroup("Exercise 2")
part2 = exercise2.getGroup("Part 2")
``` ```
@ -133,6 +147,22 @@ Downloads all test cases in the exercise group to a directory `path`. Defaults t
assignment.downloadTCs() assignment.downloadTCs()
``` ```
#### getGroup(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.
```python
# Week 1 -> Exercise 2 -> Part 2
week1 = pf.getGroups("Week 1")
exercise2 = week1.getGroup("Exercise 2")
part2 = exercise2.getGroup("Part 2")
# This is equivalent to(but faster than):
week1 = pf.getGroups("Week 1", full=True)
exercise2 = week1[1]
part2 = exercise2[1]
```
#### `submit(files)` #### `submit(files)`
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.
@ -152,3 +182,4 @@ Submits the files to the exercise group. Default arguments are `judge=True`, `wa
``` ```

View File

@ -30,10 +30,10 @@ year = themis.getYear(2023, 2024)
pf = year.getCourse("Programming Fundamentals (for CS)") pf = year.getCourse("Programming Fundamentals (for CS)")
# Get an assignment # Get an assignment
assignment = pf.getExerciseGroups() pf_assignment = pf.getGroup("Assignment 1")
# Download the files # Get a specific exercise
assignment.downloadFiles() exercise = pf_assignment.getGroup("Exercise 1")
``` ```

View File

@ -10,39 +10,39 @@ class Course:
def __init__(self, url:str, name:str, session:Session, parent): def __init__(self, url:str, name:str, session:Session, parent):
self.url = url self.url = url
self.name = name self.name = name
self.session = session self.__session = session
self.parent = parent self.__parent = parent
self.assignments = [] self.__request = self.__session.get(self.url)
self.__courseAvailable(self.session.get(self.url)) self.__raw = BeautifulSoup(self.__request.text, 'lxml')
self.__courseAvailable(self.__session.get(self.url))
def __str__(self): def __str__(self):
return f"Course {self.name} in year {self.parent.year}" return f"Course {self.name} in year {self.__parent.year}"
def __courseAvailable(self, r): def __courseAvailable(self, r):
# 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(message="'Something went wrong'. Course most likely not found. ") raise CourseUnavailable(message="'Something went wrong'. Course most likely not found. ")
@property
def info(self):
return {
"name": self.name,
"year": self.parent.year,
"url": self.url,
"assignments": [x.name for x in self.assignments]
}
def getExerciseGroups(self): def getGroups(self, full:bool=False):
r = self.session.get(self.url) section = self.__raw.find('div', class_="ass-children")
soup = BeautifulSoup(r.text, 'lxml')
section = soup.find('div', class_="ass-children")
entries = section.find_all('a', href=True) entries = section.find_all('a', href=True)
return [ return [
ExerciseGroup( ExerciseGroup(
f"https://themis.housing.rug.nl{x['href']}", f"https://themis.housing.rug.nl{x['href']}",
x, x,
self.session, self.__session,
self, self,
full
) )
for x in entries] 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)

View File

@ -6,21 +6,19 @@ from time import sleep
class ExerciseGroup: class ExerciseGroup:
def __init__(self, url: str, soup, session, parent): def __init__(self, url: str, soup, session, parent, full:bool):
self.url = url self.url = url
self.name = soup.text self.name = soup.text
self.__raw = soup self.__prev_raw = soup
self.session = session self.__session = session
self.parent = parent # This is unnecessary, but I'll keep it for now self.__request = self.__session.get(self.url)
self.request = self.session.get(self.url) self.__raw = BeautifulSoup(self.__request.text, "lxml")
self.soup = BeautifulSoup(self.request.text, "lxml") self.__full = full
def __str__(self):
return f"ExerciseGroup {self.name} in folder {self.parent.name}"
@property @property
def amExercise(self): def amExercise(self):
return "ass-submitable" in self.__raw["class"] return "ass-submitable" in self.__prev_raw["class"]
def submit(self): def submit(self):
if not self.amExercise: if not self.amExercise:
@ -31,7 +29,7 @@ class ExerciseGroup:
# Test cases # Test cases
@property @property
def testCases(self): def testCases(self):
section = self.soup.find_all("div", class_="subsec round shade") section = self.__raw.find_all("div", class_="subsec round shade")
tcs = [] tcs = []
for div in section: for div in section:
res = div.find("h4", class_="info") res = div.find("h4", class_="info")
@ -57,14 +55,14 @@ class ExerciseGroup:
print(f"Downloading {tc.text}") print(f"Downloading {tc.text}")
# download the files # download the files
with open(f"{path}/{tc.text}", "wb") as f: with open(f"{path}/{tc.text}", "wb") as f:
f.write(self.session.get(url).content) f.write(self.__session.get(url).content)
return self.testCases return self.testCases
# Files # Files
@property @property
def files(self): def files(self):
details = self.soup.find("div", id=lambda x: x and x.startswith("details")) details = self.__raw.find("div", id=lambda x: x and x.startswith("details"))
cfg_lines = details.find_all("div", class_="cfg-line") cfg_lines = details.find_all("div", class_="cfg-line")
@ -88,7 +86,7 @@ class ExerciseGroup:
print(f"Downloading file {file.text}") print(f"Downloading file {file.text}")
url = f"https://themis.housing.rug.nl{file['href']}" url = f"https://themis.housing.rug.nl{file['href']}"
with open(f"{path}/{file.text}", "wb") as f: with open(f"{path}/{file.text}", "wb") as f:
f.write(self.session.get(url).content) f.write(self.__session.get(url).content)
return self.files return self.files
@property @property
@ -96,35 +94,51 @@ class ExerciseGroup:
if self.amExercise: if self.amExercise:
return self return self
section = self.soup.find("div", class_="ass-children") section = self.__raw.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
if not self.__full:
return [(x.text,x['href']) for x in submittables]
return [ return [
ExerciseGroup( ExerciseGroup(
f"https://themis.housing.rug.nl{x['href']}", x, self.session, self f"https://themis.housing.rug.nl{x['href']}", x, self.__session, self, True
) )
for x in submittables for x in submittables
] ]
@property @property
def folders(self) -> list: def folders(self) -> list:
section = self.soup.find("div", class_="ass-children") section = self.__raw.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
if not self.__full:
return [(x.text,x['href']) for x in folders]
return [ return [
ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", x, self.session, self) ExerciseGroup(f"https://themis.housing.rug.nl{x['href']}", x, self.__session, self, True)
for x in folders 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 # Account for judge
def __raceCondition(self, soup, url:str, verbose:bool): def __raceCondition(self, soup, url:str, verbose:bool):
self.session.get(url.replace("submission", "judge")) self.__session.get(url.replace("submission", "judge"))
return self.__waitForResult(url, verbose, []) return self.__waitForResult(url, verbose, [])
def __parseTable(self, soup, url:str, verbose:bool, __printed:list): def __parseTable(self, soup, url:str, verbose:bool, __printed:list):
@ -145,15 +159,15 @@ class ExerciseGroup:
if "Passed" in status.text: if "Passed" in status.text:
fail_pass[int(name)] = True fail_pass[int(name)] = True
if int(name) not in __printed: if int(name) not in __printed and verbose == True:
print(f"{name}: ✅") print(f"{name}: ✅")
elif "Wrong output" in status.text: elif "Wrong output" in status.text:
fail_pass[int(name)] = False fail_pass[int(name)] = False
if int(name) not in __printed: if int(name) not in __printed and verbose == True:
print(f"{name}: ❌") print(f"{name}: ❌")
elif ("No status" or "error") in status.text: elif ("No status" or "error") in status.text:
fail_pass[int(name)] = None fail_pass[int(name)] = None
if int(name) not in __printed: if int(name) not in __printed and verbose == True:
print(f"{name}:🐛") print(f"{name}:🐛")
__printed.append(int(name)) __printed.append(int(name))
@ -162,7 +176,7 @@ class ExerciseGroup:
def __waitForResult(self, url:str, verbose:bool, __printed:list): def __waitForResult(self, url:str, verbose:bool, __printed:list):
# This waits for result and returns a bundled info package # This waits for result and returns a bundled info package
r = self.session.get(url) r = self.__session.get(url)
soup = BeautifulSoup(r.text, "lxml") soup = BeautifulSoup(r.text, "lxml")
return self.__parseTable(soup, url, verbose, __printed) return self.__parseTable(soup, url, verbose, __printed)
@ -173,7 +187,7 @@ class ExerciseGroup:
# Find the form with submit and store the action as url # Find the form with submit and store the action as url
# Store then the data-suffixes as file_types - dictionary # Store then the data-suffixes as file_types - dictionary
form = self.soup.find("form") form = self.__raw.find("form")
if not form: if not form:
raise IllegalAction(message="You cannot submit to this assignment.") raise IllegalAction(message="You cannot submit to this assignment.")
@ -204,7 +218,7 @@ class ExerciseGroup:
data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type} data = {"judgenow": "true" if judge else "false", "judgeLanguage": found_type}
resp = self.session.post(url, files=packaged_files, data=data) resp = self.__session.post(url, files=packaged_files, data=data)
# Close each file # Close each file
i = 0 i = 0

View File

@ -10,8 +10,8 @@ class Year:
def __init__(self, session:Session, parent, start_year:int, end_year:int): def __init__(self, session:Session, parent, start_year:int, end_year:int):
self.start = start_year self.start = start_year
self.year = end_year self.year = end_year
self.session = session
self.url = self.__constructUrl() self.url = self.__constructUrl()
self.__session = session
# Method to set the url # Method to set the url
def __constructUrl(self): def __constructUrl(self):
@ -20,7 +20,7 @@ class Year:
# Method to get the courses of the year # Method to get the courses of the year
def allCourses(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')
lis = soup.find_all('li', class_='large') lis = soup.find_all('li', class_='large')
courses = [] courses = []
@ -31,7 +31,7 @@ class Year:
Course( Course(
self.url + suffix, self.url + suffix,
li.a.text, li.a.text,
self.session, self.__session,
self self
) )
) )
@ -47,9 +47,9 @@ class Year:
def getCourse(self, name:str) -> Course: def getCourse(self, name:str) -> Course:
# Get the course # Get the course
r = self.session.get(self.url) r = self.__session.get(self.url)
soup = BeautifulSoup(r.text, 'lxml') soup = BeautifulSoup(r.text, 'lxml')
# Search by name # Search by name
course = self.url + soup.find('a', text=name)['href'].replace(f"course/{self.start}-{self.year}", "") 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 # Get the url and transform it into a course object
return Course(url=course, name=name, session=self.session, parent=self) return Course(url=course, name=name, session=self.__session, parent=self)

View File

@ -1,25 +0,0 @@
from Themis import Themis
def main():
# Debug
themis = Themis("s5230837","Bobit0Drog@231")
year = themis.getYear(2023, 2024)
# pf = year.getCourse("Programming Fundamentals (for CS)")
# pf = pf.getExerciseGroups()
# print(pf[1].exercises[1].submit("main.c")) # <- this should throw error
# no_folders = year.getCourse("Computer Architecture")
# ca_ass = no_folders.getExerciseGroups()
ai = year.getCourse("Imperative Programming (for AI)")
ai = ai.getExerciseGroups()
print(ai[7].exercises[1].submit("suitcase.py", silent=False))
ads = year.getCourse("Algorithms and Data Structures for CS")
ads = ads.getExerciseGroups()
# print(ads[0].folders)
print(ads[0].folders[5].folders[0].exercises[0].submit(["texteditor.c", "texteditor.h"], silent=False))
# for ass in ca_ass:
# print(ass.exercises)
if __name__ == "__main__":
main()

View File

@ -1,28 +0,0 @@
def suitcase(maxVolume, sizes, values, n):
dp = [[0 for _ in range(maxVolume + 1)] for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, maxVolume + 1):
if sizes[i - 1] <= j:
dp[i][j] = max(values[i - 1] + dp[i - 1][j - sizes[i - 1]], dp[i - 1][j])
else:
dp[i][j] = dp[i - 1][j]
return dp[n][maxVolume]
def main():
n, maxVolume = map(int, input().split())
sizes = []
values = []
for _ in range(n):
item, size, value = input().split()
sizes.append(int(size))
values.append(int(value))
maxSatisfaction = suitcase(maxVolume, sizes, values, n)
print(maxSatisfaction)
if __name__ == "__main__":
main()

View File

@ -1,159 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "EditOperation.h"
#include "texteditor.h"
#include "LibStack.h"
TextEditor* createTextEditor(void) {
TextEditor *editor = malloc(sizeof(*editor));
// Don't forget to initialize the data structure(s) here
editor->text = malloc(10 * sizeof(*editor->text));
editor->length = 0;
editor->capacity = 10;
return editor;
}
// Think this is correct
void insertCharacter(TextEditor* editor, int pos, char character) {
// Implement the insert operation
if (editor->length == editor->capacity) {
editor->text = realloc(editor->text, 2 * editor->capacity * sizeof(*editor->text));
editor->capacity *= 2;
}
// Shift all characters to the right
for (int i = editor->length; i > pos; i--) {
editor->text[i] = editor->text[i - 1];
}
editor->text[pos] = character;
editor->length++;
}
// This too
void deleteCharacter(TextEditor* editor, int pos) {
// Implement the delete operation
if (editor->length == 0) {
return;
}
// Shift all characters to the left
for (int i = pos; i < editor->length - 1; i++) {
editor->text[i] = editor->text[i + 1];
}
editor->length--;
}
// The issue lies within the mem allocation of the stacks
void undo(TextEditor* editor, Stack* undoStack, Stack* redoStack) {
// Optional for the bonus exercise
if (isEmptyStack(*undoStack)) {
return;
}
EditOperation operation = pop(undoStack);
if (operation.type == INSERT) {
deleteCharacter(editor, operation.position);
} else {
insertCharacter(editor, operation.position, operation.character);
}
push(operation, redoStack);
}
void redo(TextEditor* editor, Stack* undoStack, Stack* redoStack) {
// Optional for the bonus exercise
if (isEmptyStack(*redoStack)) {
return;
}
EditOperation operation = pop(redoStack);
if (operation.type == INSERT) {
insertCharacter(editor, operation.position, operation.character);
} else {
deleteCharacter(editor, operation.position);
}
push(operation, undoStack);
}
void destroyTextEditor(TextEditor* editor) {
// Free the memory allocated for the data structure(s)
free(editor->text);
free(editor);
}
void printText(TextEditor* editor) {
// Handle empty case
if (editor->length == 0) {
printf("EMPTY\n");
return;
}
// Print the text stored in the editor
for (int i = 0; i < editor->length; i++) {
printf("%c", editor->text[i]);
}
printf("\n");
}
int main(int argc, char *argv[]) {
TextEditor* editor = createTextEditor();
char command;
int pos;
char character;
// Initialize stacks
Stack undoStack;
Stack redoStack;
undoStack = newStack(1);
redoStack = newStack(1);
while(1) {
scanf(" %c", &command);
switch (command) {
// Insert a character at a given position
case 'i':
scanf("%d %c", &pos, &character);
insertCharacter(editor, pos, character);
EditOperation operation = {INSERT, character, pos};
// Stack operations
doubleStackSize(&undoStack);
push(operation, &undoStack);
break;
// Delete a character at a given position
case 'd':
scanf("%d", &pos);
character = editor->text[pos];
deleteCharacter(editor, pos);
EditOperation operation1 = {DELETE, character, pos};
doubleStackSize(&undoStack);
push(operation1, &undoStack);
break;
// Undo the last operation
case 'u':
undo(editor, &undoStack, &redoStack);
break;
// Redo the last operation
case 'r':
redo(editor, &undoStack, &redoStack);
break;
// Print and quit
case 'q':
printText(editor);
destroyTextEditor(editor);
freeStack(undoStack);
freeStack(redoStack);
return 0;
// Unknown command
default:
printf("Unknown command.\n");
break;
}
}
return 0;
}

View File

@ -1,14 +0,0 @@
#ifndef TEXTEDITOR_H
#define TEXTEDITOR_H
#include "LibStack.h"
typedef struct TextEditor {
// Store the data structure in here
char *text;
int length;
int capacity;
} TextEditor;
#endif