diff --git a/Changelog.md b/Changelog.md index 61343af..1eb355f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## [v0.3.0] +- Update routes to conform with MarkUs version 2.0+ (#35) +- Fixed bug where request errors were being hidden by json parsing errors (#34) + ## [v0.2.1] - Added new methods for managing starter files (#29) diff --git a/README.md b/README.md index 8ccac69..c14bdc7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ $ pip install markusapi * the `url` of a running [MarkUs website](https://github.com/MarkUsProject/Markus) * your `api_key` (this can be obtained from the dashboard page of the MarkUs website) +## Compatibility +* version 0.2 of this package is compatible with MarkUs version < 2.0 +* version 0.3 of this package is compatible with MarkUs version >= 2.0 + ## Usage Example Create a new Markus Api object: diff --git a/markusapi/markusapi.py b/markusapi/markusapi.py index 29af4e6..bfdfe22 100644 --- a/markusapi/markusapi.py +++ b/markusapi/markusapi.py @@ -46,98 +46,150 @@ def _auth_header(self): def _url(self, tail=""): return f"{self.url}/api/{tail}.json" + + @parse_response("json") + def get_all_courses(self) -> requests.Response: + """ + Return a list of every course in the Markus database. + Each course is a dictionary object with the keys + 'id', 'name', 'display_name', 'is_hidden', 'autotest_setting_id' + """ + return requests.get(self._url("courses"), headers=self._auth_header) + + @parse_response("json") + def new_course(self, + name: str, + display_name: str, + is_hidden: Optional[bool] = False, + autotest_setting_id: Optional[int] = None) -> requests.Response: + """ + Creates a new course in the MarkUs database. + Returns a list containing the response's status, + reason, and data. + """ + params = {"name": name, "display_name": display_name, "is_hidden": is_hidden} + if autotest_setting_id is not None: + params["autotest_setting_id"] = autotest_setting_id + return requests.post(self._url("courses"), params=params, headers=self._auth_header) + @parse_response("json") def get_all_users(self) -> requests.Response: """ Return a list of every user in the MarkUs instance. Each user is a dictionary object, with the following keys: - 'id', 'user_name', 'first_name', 'last_name', - 'type', 'grace_credits', 'notes_count', 'hidden'. + id', 'user_name', 'first_name', 'last_name', + 'type'. """ return requests.get(self._url("users"), headers=self._auth_header) @parse_response("json") - def new_user( + def new_user(self, + user_name: str, + user_type: str, + first_name: str, + last_name: str, + ) -> requests.Response: + """ + Add a new user to the MarkUs database. + Returns a list containing the response's status, + reason, and data. + """ + params = {"user_name": user_name, "type": user_type, "first_name": first_name, "last_name": last_name} + return requests.post(self._url("users"), params=params, headers=self._auth_header) + + @parse_response("json") + def get_all_roles(self, course_id: int) -> requests.Response: + """ + Return a list of every role in a particular course. + Each user is a dictionary object, with the following keys: + 'id', 'user_name', 'first_name', 'last_name', + 'type', 'grace_credits', 'hidden', 'email'. + """ + return requests.get(self._url(f"courses/{course_id}/roles"), headers=self._auth_header) + + @parse_response("json") + def new_role( self, + course_id: int, user_name: str, user_type: str, - first_name: str, - last_name: str, section_name: Optional[str] = None, grace_credits: Optional[str] = None, + hidden: Optional[bool] = False, ) -> requests.Response: """ - Add a new user to the MarkUs database. + Add a new role to a given course. Returns a list containing the response's status, reason, and data. """ - params = {"user_name": user_name, "type": user_type, "first_name": first_name, "last_name": last_name} + params = {"type": user_type, "user_name": user_name, "hidden": hidden} if section_name is not None: params["section_name"] = section_name if grace_credits is not None: params["grace_credits"] = grace_credits - return requests.post(self._url("users"), params=params, headers=self._auth_header) + return requests.post(self._url(f"courses/{course_id}/roles"), params=params, headers=self._auth_header) @parse_response("json") - def get_assignments(self) -> requests.Response: + def get_assignments(self, course_id: int) -> requests.Response: """ Return a list of all assignments. """ - return requests.get(self._url("assignments"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments"), headers=self._auth_header) @parse_response("json") - def get_groups(self, assignment_id: int) -> requests.Response: + def get_groups(self, course_id: int, assignment_id: int) -> requests.Response: """ Return a list of all groups associated with the given assignment. """ - return requests.get(self._url(f"assignments/{assignment_id}/groups"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/groups"), headers=self._auth_header) @parse_response("json") - def get_groups_by_name(self, assignment_id: int) -> requests.Response: + def get_groups_by_name(self, course_id: int, assignment_id: int) -> requests.Response: """ Return a dictionary mapping group names to group ids. """ return requests.get( - self._url(f"assignments/{assignment_id}/groups/group_ids_by_name"), headers=self._auth_header + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/group_ids_by_name"), headers=self._auth_header ) @parse_response("json") - def get_group(self, assignment_id: int, group_id: int) -> requests.Response: + def get_group(self, course_id: int, assignment_id: int, group_id: int) -> requests.Response: """ Return the group info associated with the given id and assignment. """ - return requests.get(self._url(f"assignments/{assignment_id}/groups/{group_id}"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}"), headers=self._auth_header) @parse_response("json") - def get_feedback_files(self, assignment_id: int, group_id: int) -> requests.Response: + def get_feedback_files(self, course_id: int, assignment_id: int, group_id: int) -> requests.Response: """ Get the feedback files info associated with the assignment and group. """ return requests.get( - self._url(f"assignments/{assignment_id}/groups/{group_id}/feedback_files"), headers=self._auth_header + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/feedback_files"), headers=self._auth_header ) @parse_response("content") - def get_feedback_file(self, assignment_id: int, group_id: int, feedback_file_id: int) -> requests.Response: + def get_feedback_file(self, course_id: int, assignment_id: int, group_id: int, feedback_file_id: int) -> requests.Response: """ Get the feedback file associated with the given id, assignment and group. WARNING: This will fail for non-text feedback files """ return requests.get( - self._url(f"assignments/{assignment_id}/groups/{group_id}/feedback_files/{feedback_file_id}"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/feedback_files/{feedback_file_id}"), headers=self._auth_header, ) @parse_response("text") - def get_grades_summary(self, assignment_id: int) -> requests.Response: + def get_grades_summary(self, course_id: int, assignment_id: int) -> requests.Response: """ Get grades summary csv file as a string. """ - return requests.get(self._url(f"assignments/{assignment_id}/grades_summary"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/grades_summary"), headers=self._auth_header) @parse_response("json") def new_marks_spreadsheet( self, + course_id: int, short_identifier: str, description: str = "", date: Optional[datetime] = None, @@ -156,11 +208,12 @@ def new_marks_spreadsheet( "show_total": show_total, "grade_entry_items": grade_entry_items, } - return requests.post(self._url("grade_entry_forms"), params=params, headers=self._auth_header) + return requests.post(self._url(f"courses/{course_id}/grade_entry_forms"), params=params, headers=self._auth_header) @parse_response("json") def update_marks_spreadsheet( self, + course_id: int, spreadsheet_id: int, short_identifier: Optional[str] = None, description: Optional[str] = None, @@ -173,6 +226,7 @@ def update_marks_spreadsheet( Update an existing marks spreadsheet """ params = { + "course_id": course_id, "short_identifier": short_identifier, "description": description, "date": date, @@ -183,34 +237,35 @@ def update_marks_spreadsheet( for name in list(params): if params[name] is None: params.pop(name) - return requests.put(self._url(f"grade_entry_forms/{spreadsheet_id}"), params=params, headers=self._auth_header) + return requests.put(self._url(f"courses/{course_id}/grade_entry_forms/{spreadsheet_id}"), params=params, headers=self._auth_header) @parse_response("json") def update_marks_spreadsheets_grades( - self, spreadsheet_id: int, user_name: str, grades_per_column: Dict[str, float] + self, course_id: int, spreadsheet_id: int, user_name: str, grades_per_column: Dict[str, float] ) -> requests.Response: params = {"user_name": user_name, "grade_entry_items": grades_per_column} return requests.put( - self._url(f"grade_entry_forms/{spreadsheet_id}/update_grades"), json=params, headers=self._auth_header + self._url(f"courses/{course_id}/grade_entry_forms/{spreadsheet_id}/update_grades"), json=params, headers=self._auth_header ) @parse_response("json") - def get_marks_spreadsheets(self) -> requests.Response: + def get_marks_spreadsheets(self, course_id: int) -> requests.Response: """ Get all marks spreadsheets. """ - return requests.get(self._url(f"grade_entry_forms"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/grade_entry_forms"), headers=self._auth_header) @parse_response("text") - def get_marks_spreadsheet(self, spreadsheet_id: int) -> requests.Response: + def get_marks_spreadsheet(self, course_id: int, spreadsheet_id: int) -> requests.Response: """ Get the marks spreadsheet associated with the given id. """ - return requests.get(self._url(f"grade_entry_forms/{spreadsheet_id}"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/grade_entry_forms/{spreadsheet_id}"), headers=self._auth_header) @parse_response("json") def upload_feedback_file( self, + course_id: int, assignment_id: int, group_id: int, title: str, @@ -222,6 +277,7 @@ def upload_feedback_file( Upload a feedback file to Markus. Keyword arguments: + course_id -- the course's id assignment_id -- the assignment's id group_id -- the id of the group to which we are uploading title -- the file name that will be displayed (a file extension is required) @@ -229,9 +285,9 @@ def upload_feedback_file( mime_type -- mime type of title file, if None then the mime type will be guessed based on the file extension overwrite -- whether to overwrite a feedback file with the same name that already exists in Markus """ - url_content = f"assignments/{assignment_id}/groups/{group_id}/feedback_files" + url_content = f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/feedback_files" if overwrite: - feedback_files = self.get_feedback_files(assignment_id, group_id) + feedback_files = self.get_feedback_files(course_id, assignment_id, group_id) feedback_file_id = next((ff.get("id") for ff in feedback_files if ff.get("filename") == title), None) if feedback_file_id is not None: url_content += f"/{feedback_file_id}" @@ -244,23 +300,10 @@ def upload_feedback_file( else: return requests.post(self._url(url_content), files=files, params=params, headers=self._auth_header) - @parse_response("json") - def upload_test_group_results( - self, assignment_id: int, group_id: int, test_run_id: int, test_output: Union[str, Dict] - ) -> requests.Response: - """ Upload test results to Markus """ - if not isinstance(test_output, str): - test_output = json.dumps(test_output) - params = {"test_run_id": test_run_id, "test_output": test_output} - return requests.post( - self._url(f"assignments/{assignment_id}/groups/{group_id}/test_group_results"), - json=params, - headers=self._auth_header, - ) @parse_response("json") def upload_annotations( - self, assignment_id: int, group_id: int, annotations: List, force_complete: bool = False + self, course_id: int, assignment_id: int, group_id: int, annotations: List, force_complete: bool = False ) -> requests.Response: """ Each element of annotations must be a dictionary with the following keys: @@ -276,24 +319,24 @@ def upload_annotations( """ params = {"annotations": annotations, "force_complete": force_complete} return requests.post( - self._url(f"assignments/{assignment_id}/groups/{group_id}/add_annotations"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/add_annotations"), json=params, headers=self._auth_header, ) @parse_response("json") - def get_annotations(self, assignment_id: int, group_id: Optional[int] = None) -> requests.Response: + def get_annotations(self, course_id: int, assignment_id: int, group_id: Optional[int] = None) -> requests.Response: """ Return a list of dictionaries containing information for each annotation in the assignment - with id = assignment_id. If group_id is not None, return only annotations for the given group. + with id = assignment_id in the specified course. If group_id is not None, return only annotations for the given group. """ return requests.get( - self._url(f"assignments/{assignment_id}/groups/{group_id}/annotations"), headers=self._auth_header + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/annotations"), headers=self._auth_header ) @parse_response("json") def update_marks_single_group( - self, criteria_mark_map: dict, assignment_id: int, group_id: int + self, course_id: int, criteria_mark_map: dict, assignment_id: int, group_id: int ) -> requests.Response: """ Update the marks of a single group. @@ -306,29 +349,30 @@ def update_marks_single_group( rubric level, and will be multiplied by the weight automatically. Keyword arguments: + course_id -- the course's id criteria_mark_map -- maps criteria to the desired grade assignment_id -- the assignment's id group_id -- the id of the group whose marks we are updating """ return requests.put( - self._url(f"assignments/{assignment_id}/groups/{group_id}/update_marks"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/update_marks"), json=criteria_mark_map, headers=self._auth_header, ) @parse_response("json") - def update_marking_state(self, assignment_id: int, group_id: int, new_marking_state: str) -> requests.Response: + def update_marking_state(self, course_id: int, assignment_id: int, group_id: int, new_marking_state: str) -> requests.Response: """ Update marking state for a single group to either 'complete' or 'incomplete' """ params = {"marking_state": new_marking_state} return requests.put( - self._url(f"assignments/{assignment_id}/groups/{group_id}/update_marking_state"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/update_marking_state"), params=params, headers=self._auth_header, ) @parse_response("json") def create_extra_marks( - self, assignment_id: int, group_id: int, extra_marks: float, description: str + self, course_id: int, assignment_id: int, group_id: int, extra_marks: float, description: str ) -> requests.Response: """ Create new extra mark for the particular group. @@ -336,14 +380,14 @@ def create_extra_marks( """ params = {"extra_marks": extra_marks, "description": description} return requests.post( - self._url(f"assignments/{assignment_id}/groups/{group_id}/create_extra_marks"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/create_extra_marks"), params=params, headers=self._auth_header, ) @parse_response("json") def remove_extra_marks( - self, assignment_id: int, group_id: int, extra_marks: float, description: str + self, course_id: int, assignment_id: int, group_id: int, extra_marks: float, description: str ) -> requests.Response: """ Remove the extra mark for the particular group. @@ -351,14 +395,14 @@ def remove_extra_marks( """ params = {"extra_marks": extra_marks, "description": description} return requests.delete( - self._url(f"assignments/{assignment_id}/groups/{group_id}/remove_extra_marks"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/remove_extra_marks"), params=params, headers=self._auth_header, ) @parse_response("content") def get_files_from_repo( - self, assignment_id: int, group_id: int, filename: Optional[str] = None, collected: bool = True + self, course_id: int, assignment_id: int, group_id: int, filename: Optional[str] = None, collected: bool = True ) -> requests.Response: """ Return file content from the submission of a single group. If is specified, @@ -376,16 +420,16 @@ def get_files_from_repo( if filename: params["filename"] = filename return requests.get( - self._url(f"assignments/{assignment_id}/groups/{group_id}/submission_files"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/submission_files"), params=params, headers=self._auth_header, ) @parse_response("json") - def upload_folder_to_repo(self, assignment_id: int, group_id: int, folder_path: str) -> requests.Response: + def upload_folder_to_repo(self, course_id: int, assignment_id: int, group_id: int, folder_path: str) -> requests.Response: params = {"folder_path": folder_path} return requests.post( - self._url(f"assignments/{assignment_id}/groups/{group_id}/submission_files/create_folders"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/submission_files/create_folders"), params=params, headers=self._auth_header, ) @@ -393,6 +437,7 @@ def upload_folder_to_repo(self, assignment_id: int, group_id: int, folder_path: @parse_response("json") def upload_file_to_repo( self, + course_id: int, assignment_id: int, group_id: int, file_path: str, @@ -411,14 +456,14 @@ def upload_file_to_repo( files = {"file_content": (file_path, contents)} params = {"filename": file_path, "mime_type": mime_type or mimetypes.guess_type(file_path)[0]} return requests.post( - self._url(f"assignments/{assignment_id}/groups/{group_id}/submission_files"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/submission_files"), files=files, params=params, headers=self._auth_header, ) @parse_response("json") - def remove_file_from_repo(self, assignment_id: int, group_id: int, file_path: str) -> requests.Response: + def remove_file_from_repo(self, course_id: int, assignment_id: int, group_id: int, file_path: str) -> requests.Response: """ Remove a file at file_path from the assignment directory in the repo for group with id group_id. @@ -429,13 +474,13 @@ def remove_file_from_repo(self, assignment_id: int, group_id: int, file_path: st """ params = {"filename": file_path} return requests.delete( - self._url(f"assignments/{assignment_id}/groups/{group_id}/submission_files/remove_file"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/submission_files/remove_file"), params=params, headers=self._auth_header, ) @parse_response("json") - def remove_folder_from_repo(self, assignment_id: int, group_id: int, folder_path: str) -> requests.Response: + def remove_folder_from_repo(self, course_id: int, assignment_id: int, group_id: int, folder_path: str) -> requests.Response: """ Remove a folder at folder_path and all its contents for group with id grou;_id. @@ -446,49 +491,50 @@ def remove_folder_from_repo(self, assignment_id: int, group_id: int, folder_path """ params = {"folder_path": folder_path} return requests.delete( - self._url(f"assignments/{assignment_id}/groups/{group_id}/submission_files/remove_folder"), + self._url(f"courses/{course_id}/assignments/{assignment_id}/groups/{group_id}/submission_files/remove_folder"), params=params, headers=self._auth_header, ) @parse_response("json") - def get_test_specs(self, assignment_id: int) -> requests.Response: + def get_test_specs(self, course_id: int, assignment_id: int) -> requests.Response: """ - Get the test spec settings for an assignment with id . + Get the test spec settings for an assignment with id in a course with id . """ - return requests.get(self._url(f"assignments/{assignment_id}/test_specs"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/test_specs"), headers=self._auth_header) @parse_response("json") - def update_test_specs(self, assignment_id: int, specs: Dict) -> requests.Response: + def update_test_specs(self, course_id: int, assignment_id: int, specs: Dict) -> requests.Response: """ - Update the test spec settings for an assignment with id to be . + Update the test spec settings for a course with id and + an assignment with id to be . """ params = {"specs": specs} return requests.post( - self._url(f"assignments/{assignment_id}/update_test_specs"), json=params, headers=self._auth_header + self._url(f"courses/{course_id}/assignments/{assignment_id}/update_test_specs"), json=params, headers=self._auth_header ) @parse_response("content") - def get_test_files(self, assignment_id: int) -> requests.Response: + def get_test_files(self, course_id: int, assignment_id: int) -> requests.Response: """ Return the content of a zipfile containing the content of all files uploaded for automated testing of - the assignment with id . + the assignment with id in the course with id . """ - return requests.get(self._url(f"assignments/{assignment_id}/test_files"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/test_files"), headers=self._auth_header) @parse_response("json") - def get_starter_file_entries(self, assignment_id: int, starter_file_group_id: int) -> requests.Response: + def get_starter_file_entries(self, course_id: int, starter_file_group_id: int) -> requests.Response: """ Return the name of all entries for a given starter file group. Entries are file or directory names. """ return requests.get( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/entries"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/entries"), headers=self._auth_header, ) @parse_response("json") def create_starter_file( - self, assignment_id: int, starter_file_group_id: int, file_path: str, contents: Union[str, bytes] + self, course_id: int, starter_file_group_id: int, file_path: str, contents: Union[str, bytes] ) -> requests.Response: """ Upload a starter file to the starter file group with id= for assignment with @@ -497,7 +543,7 @@ def create_starter_file( files = {"file_content": (file_path, contents)} params = {"filename": file_path} return requests.post( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/create_file"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/create_file"), params=params, files=files, headers=self._auth_header, @@ -505,35 +551,35 @@ def create_starter_file( @parse_response("json") def create_starter_folder( - self, assignment_id: int, starter_file_group_id: int, folder_path: str + self, course_id: int, starter_file_group_id: int, folder_path: str ) -> requests.Response: """ - Create a folder for the the starter file group with id= for assignment with - id=. The file_path should be a relative path from the starter file group's root directory. + Create a folder for the the starter file group with id=. + The file_path should be a relative path from the starter file group's root directory. """ params = {"folder_path": folder_path} return requests.post( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/create_folder"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/create_folder"), params=params, headers=self._auth_header, ) @parse_response("json") - def remove_starter_file(self, assignment_id: int, starter_file_group_id: int, file_path: str) -> requests.Response: + def remove_starter_file(self, course_id: int, starter_file_group_id: int, file_path: str) -> requests.Response: """ - Remove a starter file from the starter file group with id= for assignment with - id=. The file_path should be a relative path from the starter file group's root directory. + Remove a starter file from the starter file group with id=. + The file_path should be a relative path from the starter file group's root directory. """ params = {"filename": file_path} return requests.delete( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/remove_file"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/remove_file"), params=params, headers=self._auth_header, ) @parse_response("json") def remove_starter_folder( - self, assignment_id: int, starter_file_group_id: int, folder_path: str + self, course_id: int, starter_file_group_id: int, folder_path: str ) -> requests.Response: """ Remove a folder from the starter file group with id= for assignment with @@ -541,57 +587,57 @@ def remove_starter_folder( """ params = {"folder_path": folder_path} return requests.delete( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/remove_folder"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/remove_folder"), params=params, headers=self._auth_header, ) @parse_response("content") - def download_starter_file_entries(self, assignment_id: int, starter_file_group_id: int) -> requests.Response: + def download_starter_file_entries(self, course_id: int, starter_file_group_id: int) -> requests.Response: """ Return the content of a zipfile containing the content of all starter files from the starter file group with id=. """ return requests.get( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}/download_entries"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}/download_entries"), headers=self._auth_header, ) @parse_response("json") - def get_starter_file_groups(self, assignment_id: int) -> requests.Response: + def get_starter_file_groups(self, course_id: int, assignment_id: int) -> requests.Response: """ Return all starter file groups for the assignment with id= """ - return requests.get(self._url(f"assignments/{assignment_id}/starter_file_groups"), headers=self._auth_header) + return requests.get(self._url(f"courses/{course_id}/assignments/{assignment_id}/starter_file_groups"), headers=self._auth_header) @parse_response("json") - def create_starter_file_group(self, assignment_id: int) -> requests.Response: + def create_starter_file_group(self, course_id: int, assignment_id: int) -> requests.Response: """ Create a starter file groups for the assignment with id= """ - return requests.post(self._url(f"assignments/{assignment_id}/starter_file_groups"), headers=self._auth_header) + return requests.post(self._url(f"courses/{course_id}/assignments/{assignment_id}/starter_file_groups"), headers=self._auth_header) @parse_response("json") - def get_starter_file_group(self, assignment_id: int, starter_file_group_id: int) -> requests.Response: + def get_starter_file_group(self, course_id: int, starter_file_group_id: int) -> requests.Response: """ - Return the starter file group with id= for the assignment with id= + Return the starter file group with id= """ return requests.get( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}"), headers=self._auth_header, ) @parse_response("json") def update_starter_file_group( self, - assignment_id: int, + course_id: int, starter_file_group_id: int, name: Optional[str] = None, entry_rename: Optional[str] = None, use_rename: Optional[bool] = None, ) -> requests.Response: """ - Update the starter file group with id= for the assignment with id= + Update the starter file group with id= """ params = {} if name is not None: @@ -601,17 +647,17 @@ def update_starter_file_group( if use_rename is not None: params["use_rename"] = use_rename return requests.put( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}"), params=params, headers=self._auth_header, ) @parse_response("json") - def delete_starter_file_group(self, assignment_id: int, starter_file_group_id: int) -> requests.Response: + def delete_starter_file_group(self, course_id: int, starter_file_group_id: int) -> requests.Response: """ - Delete the starter file group with id= for the assignment with id= + Delete the starter file group with id= """ return requests.delete( - self._url(f"assignments/{assignment_id}/starter_file_groups/{starter_file_group_id}"), + self._url(f"courses/{course_id}/starter_file_groups/{starter_file_group_id}"), headers=self._auth_header, ) diff --git a/markusapi/response_parser.py b/markusapi/response_parser.py index b8add4a..52761e8 100644 --- a/markusapi/response_parser.py +++ b/markusapi/response_parser.py @@ -1,7 +1,12 @@ +import json from functools import wraps from typing import List, Union, Dict, Callable +class ResponseParsingException(Exception): + pass + + def parse_response(expected: str) -> Callable: """ Decorator for a function that returns a requests.Response object. @@ -22,7 +27,10 @@ def _parser(f): def _f(*args, **kwargs): response = f(*args, **kwargs) if not response.ok or expected == "json": - return response.json() + try: + return response.json() + except json.decoder.JSONDecodeError: + raise ResponseParsingException(f"status: {response.status_code}\n\n{response.text}") if expected == "content": return response.content if expected == "text": diff --git a/markusapi/tests/test_markusapi.py b/markusapi/tests/test_markusapi.py index 998aacb..f77321e 100644 --- a/markusapi/tests/test_markusapi.py +++ b/markusapi/tests/test_markusapi.py @@ -65,6 +65,32 @@ def test_called_with_correct_url(self, response_mock, basic_call): assert args[0] == f"{FAKE_URL}/api/{self.url}.json" +class TestGetAllCourses(AbstractTestClass): + request_verb = "get" + response_format = "json" + url = "courses" + + @staticmethod + @pytest.fixture + def basic_call(api): + return api.get_all_courses() + +class TestNewCourse(AbstractTestClass): + request_verb = "post" + response_format = "json" + url = "courses" + + @staticmethod + @pytest.fixture + def basic_call(api): + yield api.new_course("CSC108", "CSC 108 Winter 2022") + + def test_called_with_basic_params(self, api, response_mock): + api.new_course('CSC108', 'CSC 108 Winter 2022') + params = {"name": "CSC108", "display_name": "CSC 108 Winter 2022", "is_hidden": False} + _, kwargs = response_mock.call_args + assert kwargs["params"] == params + class TestGetAllUsers(AbstractTestClass): request_verb = "get" response_format = "json" @@ -75,7 +101,6 @@ class TestGetAllUsers(AbstractTestClass): def basic_call(api): return api.get_all_users() - class TestNewUser(AbstractTestClass): request_verb = "post" response_format = "json" @@ -84,34 +109,59 @@ class TestNewUser(AbstractTestClass): @staticmethod @pytest.fixture def basic_call(api): - return api.new_user("test", "Student", "first", "last", "section", "3") + yield api.new_user("test", "EndUser", "first", "last") def test_called_with_basic_params(self, api, response_mock): - api.new_user("test", "Student", "first", "last") - params = {"user_name": "test", "type": "Student", "first_name": "first", "last_name": "last"} + api.new_user("test", "EndUser", "first", "last") + params = {"user_name": "test", "type": "EndUser", "first_name": "first", "last_name": "last"} + _, kwargs = response_mock.call_args + assert kwargs["params"] == params + +class TestGetAllRoles(AbstractTestClass): + request_verb = "get" + response_format = "json" + url = "courses/1/roles" + + @staticmethod + @pytest.fixture + def basic_call(api): + return api.get_all_roles(1) + + +class TestNewRole(AbstractTestClass): + request_verb = "post" + response_format = "json" + url = "courses/1/roles" + + @staticmethod + @pytest.fixture + def basic_call(api): + return api.new_role("1", "test", "Student", "section", "3") + + def test_called_with_basic_params(self, api, response_mock): + api.new_role("1", "test", "Student") + params = {"user_name": "test", "type": "Student", "hidden": False} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_called_with_section(self, api, response_mock): - api.new_user("test", "Student", "first", "last", section_name="section") + api.new_role("1", "test", "Student", section_name="section") params = { "user_name": "test", "type": "Student", - "first_name": "first", - "last_name": "last", "section_name": "section", + "hidden": False, } _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_called_with_grace_credits(self, api, response_mock): - api.new_user("test", "Student", "first", "last", grace_credits="3") + api.new_role("1", "test", "Student", grace_credits="3") params = { - "user_name": "test", "type": "Student", - "first_name": "first", - "last_name": "last", + "user_name": "test", "grace_credits": "3", + "hidden": False, } _, kwargs = response_mock.call_args assert kwargs["params"] == params @@ -120,93 +170,93 @@ def test_called_with_grace_credits(self, api, response_mock): class TestGetAssignments(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments" + url = "courses/1/assignments" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_assignments() + yield api.get_assignments(1) class TestGetGroups(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/groups" + url = "courses/1/assignments/1/groups" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_groups(1) + yield api.get_groups(1, 1) class TestGetGroupsByName(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/groups/group_ids_by_name" + url = "courses/1/assignments/1/groups/group_ids_by_name" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_groups_by_name(1) + yield api.get_groups_by_name(1, 1) class TestGetGroup(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/groups/1" + url = "courses/1/assignments/1/groups/1" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_group(1, 1) + yield api.get_group(1, 1, 1) class TestGetFeedbackFiles(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/groups/1/feedback_files" + url = "courses/1/assignments/1/groups/1/feedback_files" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_feedback_files(1, 1) + yield api.get_feedback_files(1, 1, 1) class TestGetFeedbackFile(AbstractTestClass): request_verb = "get" response_format = "content" - url = "assignments/1/groups/1/feedback_files/1" + url = "courses/1/assignments/1/groups/1/feedback_files/1" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_feedback_file(1, 1, 1) + yield api.get_feedback_file(1, 1, 1, 1) class TestGetGradesSummary(AbstractTestClass): request_verb = "get" response_format = "text" - url = "assignments/1/grades_summary" + url = "courses/1/assignments/1/grades_summary" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_grades_summary(1) + yield api.get_grades_summary(1, 1) class TestNewMarksSpreadsheet(AbstractTestClass): request_verb = "post" response_format = "json" - url = "grade_entry_forms" + url = "courses/1/grade_entry_forms" @staticmethod @pytest.fixture def basic_call(api): - yield api.new_marks_spreadsheet("test", "description", datetime.datetime.now()) + yield api.new_marks_spreadsheet(1, "test", "description", datetime.datetime.now()) def test_called_with_basic_params(self, api, response_mock): now = datetime.datetime.now() - api.new_marks_spreadsheet("test", "description", now) + api.new_marks_spreadsheet(1, "test", "description", now) params = { "short_identifier": "test", "description": "description", @@ -220,7 +270,7 @@ def test_called_with_basic_params(self, api, response_mock): def test_called_with_is_hidden(self, api, response_mock): now = datetime.datetime.now() - api.new_marks_spreadsheet("test", "description", now, is_hidden=False) + api.new_marks_spreadsheet(1, "test", "description", now, is_hidden=False) params = { "short_identifier": "test", "description": "description", @@ -234,7 +284,7 @@ def test_called_with_is_hidden(self, api, response_mock): def test_called_with_is_show_total(self, api, response_mock): now = datetime.datetime.now() - api.new_marks_spreadsheet("test", "description", now, show_total=False) + api.new_marks_spreadsheet(1, "test", "description", now, show_total=False) params = { "short_identifier": "test", "description": "description", @@ -249,7 +299,7 @@ def test_called_with_is_show_total(self, api, response_mock): def test_called_with_is_show_grade_entry_items(self, api, response_mock): now = datetime.datetime.now() ge_items = [{"name": "a", "out_of": 4}] - api.new_marks_spreadsheet("test", "description", now, grade_entry_items=ge_items) + api.new_marks_spreadsheet(1, "test", "description", now, grade_entry_items=ge_items) params = { "short_identifier": "test", "description": "description", @@ -265,39 +315,40 @@ def test_called_with_is_show_grade_entry_items(self, api, response_mock): class TestUpdateMarksSpreadsheet(AbstractTestClass): request_verb = "put" response_format = "json" - url = "grade_entry_forms/1" + url = "courses/1/grade_entry_forms/1" @staticmethod @pytest.fixture def basic_call(api): - yield api.update_marks_spreadsheet(1, "test", "description", datetime.datetime.now()) + yield api.update_marks_spreadsheet(1, 1, "test", "description", datetime.datetime.now()) def test_called_with_basic_params(self, api, response_mock): now = datetime.datetime.now() - api.update_marks_spreadsheet(1, "test", "description", now) - params = {"short_identifier": "test", "description": "description", "date": now} + api.update_marks_spreadsheet(1, 1, "test", "description", now) + params = {"course_id": 1, "short_identifier": "test", "description": "description", "date": now} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_called_with_is_hidden(self, api, response_mock): now = datetime.datetime.now() - api.update_marks_spreadsheet(1, "test", "description", now, is_hidden=False) - params = {"short_identifier": "test", "description": "description", "date": now, "is_hidden": False} + api.update_marks_spreadsheet(1, 1, "test", "description", now, is_hidden=False) + params = {"course_id": 1, "short_identifier": "test", "description": "description", "date": now, "is_hidden": False} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_called_with_is_show_total(self, api, response_mock): now = datetime.datetime.now() - api.update_marks_spreadsheet(1, "test", "description", now, show_total=False) - params = {"short_identifier": "test", "description": "description", "date": now, "show_total": False} + api.update_marks_spreadsheet(1, 1, "test", "description", now, show_total=False) + params = {"course_id": 1, "short_identifier": "test", "description": "description", "date": now, "show_total": False} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_called_with_is_show_grade_entry_items(self, api, response_mock): now = datetime.datetime.now() ge_items = [{"name": "a", "out_of": 4}] - api.update_marks_spreadsheet(1, "test", "description", now, grade_entry_items=ge_items) + api.update_marks_spreadsheet(1, 1, "test", "description", now, grade_entry_items=ge_items) params = { + "course_id": 1, "short_identifier": "test", "description": "description", "date": now, @@ -310,15 +361,15 @@ def test_called_with_is_show_grade_entry_items(self, api, response_mock): class TestUpdateMarksSpreadsheetGrades(AbstractTestClass): request_verb = "put" response_format = "json" - url = "grade_entry_forms/1/update_grades" + url = "courses/1/grade_entry_forms/1/update_grades" @staticmethod @pytest.fixture def basic_call(api): - yield api.update_marks_spreadsheets_grades(1, "some_user", {"some_column": 2}) + yield api.update_marks_spreadsheets_grades(1, 1, "some_user", {"some_column": 2}) def test_called_with_basic_params(self, api, response_mock): - api.update_marks_spreadsheets_grades(1, "some_user", {"some_column": 2}) + api.update_marks_spreadsheets_grades(1, 1, "some_user", {"some_column": 2}) params = {"user_name": "some_user", "grade_entry_items": {"some_column": 2}} _, kwargs = response_mock.call_args assert kwargs["json"] == params @@ -327,52 +378,52 @@ def test_called_with_basic_params(self, api, response_mock): class TestGetMarksSpreadsheets(AbstractTestClass): request_verb = "get" response_format = "json" - url = "grade_entry_forms" + url = "courses/1/grade_entry_forms" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_marks_spreadsheets() + yield api.get_marks_spreadsheets(1) class TestGetMarksSpreadsheet(AbstractTestClass): request_verb = "get" response_format = "text" - url = "grade_entry_forms/1" + url = "courses/1/grade_entry_forms/1" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_marks_spreadsheet(1) + yield api.get_marks_spreadsheet(1, 1) class TestUploadFeedbackFileReplace(AbstractTestClass): request_verb = "put" response_format = "json" - url = "assignments/1/groups/1/feedback_files/1" + url = "courses/1/assignments/1/groups/1/feedback_files/1" @staticmethod @pytest.fixture def basic_call(api): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "test.txt"}]) - yield api.upload_feedback_file(1, 1, "test.txt", "feedback info") + yield api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info") def test_discovers_mime_type(self, api, response_mock): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "test.txt"}]) - api.upload_feedback_file(1, 1, "test.txt", "feedback info") + api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info") _, kwargs = response_mock.call_args assert kwargs["params"]["mime_type"] == "text/plain" def test_called_with_mime_type(self, api, response_mock): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "test.txt"}]) - api.upload_feedback_file(1, 1, "test.txt", "feedback info", mime_type="application/octet-stream") + api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info", mime_type="application/octet-stream") params = {"filename": "test.txt", "mime_type": "application/octet-stream"} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_sends_file_data(self, api, response_mock): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "test.txt"}]) - api.upload_feedback_file(1, 1, "test.txt", "feedback info") + api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info") files = {"file_content": ("test.txt", "feedback info")} _, kwargs = response_mock.call_args assert kwargs["files"] == files @@ -381,13 +432,13 @@ def test_sends_file_data(self, api, response_mock): class TestUploadFeedbackFileNew(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/groups/1/feedback_files" + url = "courses/1/assignments/1/groups/1/feedback_files" @staticmethod @pytest.fixture def basic_call(api): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "other.txt"}]) - yield api.upload_feedback_file(1, 1, "test.txt", "feedback info") + yield api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info") class TestUploadFeedbackFileNoOverwrite(TestUploadFeedbackFileNew): @@ -395,46 +446,13 @@ class TestUploadFeedbackFileNoOverwrite(TestUploadFeedbackFileNew): @pytest.fixture def basic_call(api): api.get_feedback_files = Mock(return_value=[{"id": 1, "filename": "test.txt"}]) - yield api.upload_feedback_file(1, 1, "test.txt", "feedback info", overwrite=False) - - -class TestUploadTestGroupResultsJsonString(AbstractTestClass): - request_verb = "post" - response_format = "json" - url = "assignments/1/groups/1/test_group_results" - - @staticmethod - @pytest.fixture - def basic_call(api): - yield api.upload_test_group_results(1, 1, 1, '{"data": []}') - - def test_called_wth_basic_args(self, api, response_mock): - api.upload_test_group_results(1, 1, 1, '{"data": []}') - params = {"test_run_id": 1, "test_output": '{"data": []}'} - _, kwargs = response_mock.call_args - assert kwargs["json"] == params - - -class TestUploadTestGroupResultsDict(AbstractTestClass): - request_verb = "post" - response_format = "json" - url = "assignments/1/groups/1/test_group_results" - - @staticmethod - @pytest.fixture - def basic_call(api): - yield api.upload_test_group_results(1, 1, 1, {"data": []}) - - def test_dict_changed_to_json_string(self, api, response_mock): - api.upload_test_group_results(1, 1, 1, {"data": []}) - _, kwargs = response_mock.call_args - assert kwargs["json"]["test_output"] == '{"data": []}' + yield api.upload_feedback_file(1, 1, 1, "test.txt", "feedback info", overwrite=False) class TestUploadAnnotations(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/groups/1/add_annotations" + url = "courses/1/assignments/1/groups/1/add_annotations" annotations = [ { "filename": "test.txt", @@ -450,16 +468,16 @@ class TestUploadAnnotations(AbstractTestClass): @classmethod @pytest.fixture def basic_call(cls, api): - yield api.upload_annotations(1, 1, cls.annotations) + yield api.upload_annotations(1, 1, 1, cls.annotations) def test_called_with_basic_params(self, api, response_mock): - api.upload_annotations(1, 1, self.annotations) + api.upload_annotations(1, 1, 1, self.annotations) params = {"annotations": self.annotations, "force_complete": False} _, kwargs = response_mock.call_args assert kwargs["json"] == params def test_called_with_force_complete(self, api, response_mock): - api.upload_annotations(1, 1, self.annotations, True) + api.upload_annotations(1, 1, 1, self.annotations, True) params = {"annotations": self.annotations, "force_complete": True} _, kwargs = response_mock.call_args assert kwargs["json"] == params @@ -468,26 +486,26 @@ def test_called_with_force_complete(self, api, response_mock): class TestGetAnnotations(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/groups/1/annotations" + url = "courses/1/assignments/1/groups/1/annotations" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_annotations(1, 1) + yield api.get_annotations(1, 1, 1) class TestUpdateMarksSingleGroup(AbstractTestClass): request_verb = "put" response_format = "json" - url = "assignments/1/groups/1/update_marks" + url = "courses/1/assignments/1/groups/1/update_marks" @staticmethod @pytest.fixture def basic_call(api): - yield api.update_marks_single_group({"criteria_a": 10}, 1, 1) + yield api.update_marks_single_group(1, {"criteria_a": 10}, 1, 1) def test_called_with_basic_params(self, api, response_mock): - api.update_marks_single_group({"criteria_a": 10}, 1, 1) + api.update_marks_single_group(1, {"criteria_a": 10}, 1, 1) _, kwargs = response_mock.call_args assert kwargs["json"] == {"criteria_a": 10} @@ -495,15 +513,15 @@ def test_called_with_basic_params(self, api, response_mock): class TestUpdateMarkingState(AbstractTestClass): request_verb = "put" response_format = "json" - url = "assignments/1/groups/1/update_marking_state" + url = "courses/1/assignments/1/groups/1/update_marking_state" @staticmethod @pytest.fixture def basic_call(api): - yield api.update_marking_state(1, 1, "collected") + yield api.update_marking_state(1, 1, 1, "collected") def test_called_with_basic_params(self, api, response_mock): - api.update_marking_state(1, 1, "collected") + api.update_marking_state(1, 1, 1, "collected") _, kwargs = response_mock.call_args assert kwargs["params"] == {"marking_state": "collected"} @@ -511,15 +529,15 @@ def test_called_with_basic_params(self, api, response_mock): class TestCreateExtraMarks(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/groups/1/create_extra_marks" + url = "courses/1/assignments/1/groups/1/create_extra_marks" @staticmethod @pytest.fixture def basic_call(api): - yield api.create_extra_marks(1, 1, 10, "a bonus!") + yield api.create_extra_marks(1, 1, 1, 10, "a bonus!") def test_called_with_basic_params(self, api, response_mock): - api.create_extra_marks(1, 1, 10, "a bonus!") + api.create_extra_marks(1, 1, 1, 10, "a bonus!") _, kwargs = response_mock.call_args assert kwargs["params"] == {"extra_marks": 10, "description": "a bonus!"} @@ -527,15 +545,15 @@ def test_called_with_basic_params(self, api, response_mock): class TestRemoveExtraMarks(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/groups/1/remove_extra_marks" + url = "courses/1/assignments/1/groups/1/remove_extra_marks" @staticmethod @pytest.fixture def basic_call(api): - yield api.remove_extra_marks(1, 1, 10, "a bonus!") + yield api.remove_extra_marks(1, 1, 1, 10, "a bonus!") def test_called_with_basic_params(self, api, response_mock): - api.remove_extra_marks(1, 1, 10, "a bonus!") + api.remove_extra_marks(1, 1, 1, 10, "a bonus!") _, kwargs = response_mock.call_args assert kwargs["params"] == {"extra_marks": 10, "description": "a bonus!"} @@ -543,25 +561,25 @@ def test_called_with_basic_params(self, api, response_mock): class TestGetFilesFromRepo(AbstractTestClass): request_verb = "get" response_format = "content" - url = "assignments/1/groups/1/submission_files" + url = "courses/1/assignments/1/groups/1/submission_files" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_files_from_repo(1, 1) + yield api.get_files_from_repo(1, 1, 1) def test_called_with_basic_params(self, api, response_mock): - api.get_files_from_repo(1, 1) + api.get_files_from_repo(1, 1, 1) _, kwargs = response_mock.call_args assert kwargs["params"] == {"collected": True} def test_called_with_collected(self, api, response_mock): - api.get_files_from_repo(1, 1, collected=False) + api.get_files_from_repo(1, 1, 1, collected=False) _, kwargs = response_mock.call_args assert kwargs["params"] == {} def test_called_with_filename(self, api, response_mock): - api.get_files_from_repo(1, 1, filename="test.txt") + api.get_files_from_repo(1, 1, 1, filename="test.txt") _, kwargs = response_mock.call_args assert kwargs["params"] == {"collected": True, "filename": "test.txt"} @@ -569,15 +587,15 @@ def test_called_with_filename(self, api, response_mock): class TestUploadFolderToRepo(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/groups/1/submission_files/create_folders" + url = "courses/1/assignments/1/groups/1/submission_files/create_folders" @staticmethod @pytest.fixture def basic_call(api): - yield api.upload_folder_to_repo(1, 1, "subdir") + yield api.upload_folder_to_repo(1, 1, 1, "subdir") def test_called_with_basic_params(self, api, response_mock): - api.upload_folder_to_repo(1, 1, "subdir") + api.upload_folder_to_repo(1, 1, 1, "subdir") _, kwargs = response_mock.call_args assert kwargs["params"] == {"folder_path": "subdir"} @@ -585,26 +603,26 @@ def test_called_with_basic_params(self, api, response_mock): class TestUploadFileToRepo(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/groups/1/submission_files" + url = "courses/1/assignments/1/groups/1/submission_files" @staticmethod @pytest.fixture def basic_call(api): - yield api.upload_file_to_repo(1, 1, "test.txt", "some content") + yield api.upload_file_to_repo(1, 1, 1, "test.txt", "some content") def test_discovers_mime_type(self, api, response_mock): - api.upload_file_to_repo(1, 1, "test.txt", "some content") + api.upload_file_to_repo(1, 1, 1, "test.txt", "some content") _, kwargs = response_mock.call_args assert kwargs["params"]["mime_type"] == "text/plain" def test_called_with_mime_type(self, api, response_mock): - api.upload_file_to_repo(1, 1, "test.txt", "feedback info", mime_type="application/octet-stream") + api.upload_file_to_repo(1, 1, 1, "test.txt", "feedback info", mime_type="application/octet-stream") params = {"filename": "test.txt", "mime_type": "application/octet-stream"} _, kwargs = response_mock.call_args assert kwargs["params"] == params def test_sends_file_data(self, api, response_mock): - api.upload_file_to_repo(1, 1, "test.txt", "some content") + api.upload_file_to_repo(1, 1, 1, "test.txt", "some content") files = {"file_content": ("test.txt", "some content")} _, kwargs = response_mock.call_args assert kwargs["files"] == files @@ -613,15 +631,15 @@ def test_sends_file_data(self, api, response_mock): class TestRemoveFileFromRepo(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/groups/1/submission_files/remove_file" + url = "courses/1/assignments/1/groups/1/submission_files/remove_file" @staticmethod @pytest.fixture def basic_call(api): - yield api.remove_file_from_repo(1, 1, "test.txt") + yield api.remove_file_from_repo(1, 1, 1, "test.txt") def test_called_with_basic_params(self, api, response_mock): - api.remove_file_from_repo(1, 1, "test.txt") + api.remove_file_from_repo(1, 1, 1, "test.txt") _, kwargs = response_mock.call_args assert kwargs["params"] == {"filename": "test.txt"} @@ -629,15 +647,15 @@ def test_called_with_basic_params(self, api, response_mock): class TestRemoveFolderFromRepo(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/groups/1/submission_files/remove_folder" + url = "courses/1/assignments/1/groups/1/submission_files/remove_folder" @staticmethod @pytest.fixture def basic_call(api): - yield api.remove_folder_from_repo(1, 1, "subdir") + yield api.remove_folder_from_repo(1, 1, 1, "subdir") def test_called_with_basic_params(self, api, response_mock): - api.remove_folder_from_repo(1, 1, "subdir") + api.remove_folder_from_repo(1, 1, 1, "subdir") _, kwargs = response_mock.call_args assert kwargs["params"] == {"folder_path": "subdir"} @@ -645,27 +663,27 @@ def test_called_with_basic_params(self, api, response_mock): class TestGetTestSpecs(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/test_specs" + url = "courses/1/assignments/1/test_specs" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_test_specs(1) + yield api.get_test_specs(1, 1) class TestUpdateTestSpecs(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/update_test_specs" + url = "courses/1/assignments/1/update_test_specs" @staticmethod @pytest.fixture def basic_call(api): - yield api.update_test_specs(1, {}) + yield api.update_test_specs(1, 1, {}) def test_called_with_basic_params(self, api, response_mock): specs = {"some": ["fake", "data"]} - api.update_test_specs(1, specs) + api.update_test_specs(1, 1, specs) _, kwargs = response_mock.call_args assert kwargs["json"] == {"specs": specs} @@ -673,18 +691,18 @@ def test_called_with_basic_params(self, api, response_mock): class TestGetTestFiles(AbstractTestClass): request_verb = "get" response_format = "content" - url = "assignments/1/test_files" + url = "courses/1/assignments/1/test_files" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_test_files(1) + yield api.get_test_files(1, 1) class TestGetStarterFileEntries(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/starter_file_groups/1/entries" + url = "courses/1/starter_file_groups/1/entries" @staticmethod @pytest.fixture @@ -695,7 +713,7 @@ def basic_call(api): class TestCreateStarterFile(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/starter_file_groups/1/create_file" + url = "courses/1/starter_file_groups/1/create_file" @staticmethod @pytest.fixture @@ -712,7 +730,7 @@ def test_called_with_filename_and_content(self, api, response_mock): class TestCreateStarterFolder(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/starter_file_groups/1/create_folder" + url = "courses/1/starter_file_groups/1/create_folder" @staticmethod @pytest.fixture @@ -728,7 +746,7 @@ def test_called_with_filename_and_content(self, api, response_mock): class TestRemoveStarterFile(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/starter_file_groups/1/remove_file" + url = "courses/1/starter_file_groups/1/remove_file" @staticmethod @pytest.fixture @@ -744,7 +762,7 @@ def test_called_with_filename(self, api, response_mock): class TestRemoveStarterFolder(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/starter_file_groups/1/remove_folder" + url = "courses/1/starter_file_groups/1/remove_folder" @staticmethod @pytest.fixture @@ -760,7 +778,7 @@ def test_called_with_filename_and_content(self, api, response_mock): class TestDownloadStarterFileEntries(AbstractTestClass): request_verb = "get" response_format = "content" - url = "assignments/1/starter_file_groups/1/download_entries" + url = "courses/1/starter_file_groups/1/download_entries" @staticmethod @pytest.fixture @@ -771,29 +789,29 @@ def basic_call(api): class TestDownloadStarterFileGroups(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/starter_file_groups" + url = "courses/1/assignments/1/starter_file_groups" @staticmethod @pytest.fixture def basic_call(api): - yield api.get_starter_file_groups(1) + yield api.get_starter_file_groups(1, 1) class TestCreateStarterFileGroup(AbstractTestClass): request_verb = "post" response_format = "json" - url = "assignments/1/starter_file_groups" + url = "courses/1/assignments/1/starter_file_groups" @staticmethod @pytest.fixture def basic_call(api): - yield api.create_starter_file_group(1) + yield api.create_starter_file_group(1, 1) class TestGetStarterFileGroup(AbstractTestClass): request_verb = "get" response_format = "json" - url = "assignments/1/starter_file_groups/1" + url = "courses/1/starter_file_groups/1" @staticmethod @pytest.fixture @@ -804,7 +822,7 @@ def basic_call(api): class TestUpdateStarterFileGroup(AbstractTestClass): request_verb = "put" response_format = "json" - url = "assignments/1/starter_file_groups/1" + url = "courses/1/starter_file_groups/1" @staticmethod @pytest.fixture @@ -830,7 +848,7 @@ def test_called_with_use_rename(self, api, response_mock): class TestDeleteStarterFileGroup(AbstractTestClass): request_verb = "delete" response_format = "json" - url = "assignments/1/starter_file_groups/1" + url = "courses/1/starter_file_groups/1" @staticmethod @pytest.fixture diff --git a/setup.py b/setup.py index 7861fb8..6a9d5be 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ setuptools.setup( name="markusapi", - version="0.2.1", - author="Alessio Di Sandro, Misha Schwartz", + version="0.3.0", + author="Alessio Di Sandro, Misha Schwartz, Sam Maldonado", author_email="mschwa@cs.toronto.edu", description="Interface to interact with MarkUs API", long_description=long_description,