diff --git a/docs/api.md b/docs/api.md index a113413..df83064 100644 --- a/docs/api.md +++ b/docs/api.md @@ -61,39 +61,48 @@ courses = year.allCourses() pf = year.getCourse("Programming Fundamentals (for CS)") print(pf.info) # <- course info attribute -assignments = pf.getExerciseGroups() +assignments = pf.getGroups() ``` ### Methods -#### `getExerciseGroups()` -Returns a list of `ExerciseGroup` instances corresponding to all exercise groups visible to the user in a given `Course`. +#### `getGroups(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. + +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 -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` -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. * 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: ``` @@ -112,9 +121,14 @@ 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] +week1 = assignments[0] # Week 1 +exercise2 = week1.folders[1] # Exercise 2 +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() ``` +#### 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)` 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 ``` + diff --git a/docs/index.md b/docs/index.md index 15cad7a..e6d6b9b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,10 +30,10 @@ year = themis.getYear(2023, 2024) pf = year.getCourse("Programming Fundamentals (for CS)") # Get an assignment -assignment = pf.getExerciseGroups() +pf_assignment = pf.getGroup("Assignment 1") -# Download the files -assignment.downloadFiles() +# Get a specific exercise +exercise = pf_assignment.getGroup("Exercise 1") ``` diff --git a/src/Course.py b/src/Course.py index 5ad5e6b..9a04743 100644 --- a/src/Course.py +++ b/src/Course.py @@ -10,39 +10,39 @@ class Course: def __init__(self, url:str, name:str, session:Session, parent): self.url = url self.name = name - self.session = session - self.parent = parent - self.assignments = [] - self.__courseAvailable(self.session.get(self.url)) + 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}" + 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. ") - - @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): - r = self.session.get(self.url) - soup = BeautifulSoup(r.text, 'lxml') - section = soup.find('div', class_="ass-children") + 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.__session, self, + full ) - for x in entries] \ No newline at end of file + 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) diff --git a/src/ExerciseGroup.py b/src/ExerciseGroup.py index f55616d..9b46afe 100644 --- a/src/ExerciseGroup.py +++ b/src/ExerciseGroup.py @@ -6,21 +6,19 @@ from time import sleep class ExerciseGroup: - def __init__(self, url: str, soup, session, parent): + def __init__(self, url: str, soup, session, parent, full:bool): self.url = url self.name = soup.text - self.__raw = soup - self.session = session - self.parent = parent # This is unnecessary, but I'll keep it for now - self.request = self.session.get(self.url) - self.soup = BeautifulSoup(self.request.text, "lxml") + self.__prev_raw = soup + self.__session = session + self.__request = self.__session.get(self.url) + self.__raw = BeautifulSoup(self.__request.text, "lxml") + self.__full = full - def __str__(self): - return f"ExerciseGroup {self.name} in folder {self.parent.name}" @property def amExercise(self): - return "ass-submitable" in self.__raw["class"] + return "ass-submitable" in self.__prev_raw["class"] def submit(self): if not self.amExercise: @@ -31,7 +29,7 @@ class ExerciseGroup: # Test cases @property def testCases(self): - section = self.soup.find_all("div", class_="subsec round shade") + section = self.__raw.find_all("div", class_="subsec round shade") tcs = [] for div in section: res = div.find("h4", class_="info") @@ -57,14 +55,14 @@ class ExerciseGroup: print(f"Downloading {tc.text}") # download the files 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 # Files @property 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") @@ -88,7 +86,7 @@ class ExerciseGroup: 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) + f.write(self.__session.get(url).content) return self.files @property @@ -96,35 +94,51 @@ class ExerciseGroup: if self.amExercise: return self - section = self.soup.find("div", class_="ass-children") + 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 + f"https://themis.housing.rug.nl{x['href']}", x, self.__session, self, True ) for x in submittables ] @property def folders(self) -> list: - section = self.soup.find("div", class_="ass-children") + 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) + 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")) + self.__session.get(url.replace("submission", "judge")) return self.__waitForResult(url, verbose, []) def __parseTable(self, soup, url:str, verbose:bool, __printed:list): @@ -145,15 +159,15 @@ class ExerciseGroup: if "Passed" in status.text: fail_pass[int(name)] = True - if int(name) not in __printed: + 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: + 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: + if int(name) not in __printed and verbose == True: print(f"{name}:🐛") __printed.append(int(name)) @@ -162,7 +176,7 @@ class ExerciseGroup: def __waitForResult(self, url:str, verbose:bool, __printed:list): # 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") return self.__parseTable(soup, url, verbose, __printed) @@ -173,7 +187,7 @@ class ExerciseGroup: # Find the form with submit and store the action as url # Store then the data-suffixes as file_types - dictionary - form = self.soup.find("form") + form = self.__raw.find("form") if not form: 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} - resp = self.session.post(url, files=packaged_files, data=data) + resp = self.__session.post(url, files=packaged_files, data=data) # Close each file i = 0 diff --git a/src/Year.py b/src/Year.py index d89b298..639c994 100644 --- a/src/Year.py +++ b/src/Year.py @@ -10,8 +10,8 @@ class Year: def __init__(self, session:Session, parent, start_year:int, end_year:int): self.start = start_year self.year = end_year - self.session = session self.url = self.__constructUrl() + self.__session = session # Method to set the url def __constructUrl(self): @@ -20,7 +20,7 @@ class 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) + r = self.__session.get(self.url) soup = BeautifulSoup(r.text, 'lxml') lis = soup.find_all('li', class_='large') courses = [] @@ -31,7 +31,7 @@ class Year: Course( self.url + suffix, li.a.text, - self.session, + self.__session, self ) ) @@ -47,9 +47,9 @@ class Year: def getCourse(self, name:str) -> Course: # Get the course - r = self.session.get(self.url) + 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) \ No newline at end of file + return Course(url=course, name=name, session=self.__session, parent=self) \ No newline at end of file diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 9f91e78..0000000 --- a/src/main.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/src/suitcase.py b/src/suitcase.py deleted file mode 100644 index 7f5aa45..0000000 --- a/src/suitcase.py +++ /dev/null @@ -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() diff --git a/src/texteditor.c b/src/texteditor.c deleted file mode 100644 index b658edb..0000000 --- a/src/texteditor.c +++ /dev/null @@ -1,159 +0,0 @@ -#include -#include -#include -#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; -} - diff --git a/src/texteditor.h b/src/texteditor.h deleted file mode 100644 index 04ebccc..0000000 --- a/src/texteditor.h +++ /dev/null @@ -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 -